feat: add post like
This commit is contained in:
@@ -21,9 +21,10 @@
|
||||
* see the in-place auth modal before the underlying action runs.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import {
|
||||
HiHeart,
|
||||
HiOutlineArrowPath,
|
||||
HiOutlineChatBubbleOvalLeft,
|
||||
HiOutlineFolderPlus,
|
||||
@@ -37,6 +38,7 @@ import { useAuth } from "@/components/auth/auth-provider";
|
||||
import { useAuthGate } from "@/components/auth/auth-gate";
|
||||
import { useMediaQuery } from "@/components/selection-toolbar/shared";
|
||||
import { CollectionsDrawer } from "@/components/selection-toolbar/collections-drawer";
|
||||
import { PostLikesDrawer } from "@/components/social/post-likes-drawer";
|
||||
import { useSharePost } from "./share-post-provider";
|
||||
|
||||
// All icons come from the Heroicons v2 outline family (react-icons
|
||||
@@ -60,6 +62,12 @@ export function PostActionsFooter({ post }: { post: Post }) {
|
||||
const [reshared, setReshared] = useState<boolean>(
|
||||
!!post.viewer_has_reshared,
|
||||
);
|
||||
// Like state — seeded from the hydrated post, toggled optimistically and
|
||||
// reconciled to the server's effective count on success.
|
||||
const [liked, setLiked] = useState<boolean>(!!post.viewer_has_liked);
|
||||
const [likeCount, setLikeCount] = useState<number>(post.likes_count ?? 0);
|
||||
const [likesOpen, setLikesOpen] = useState(false);
|
||||
const likeBusy = useRef(false);
|
||||
|
||||
const isOwner = !!user && user.id === post.user_id;
|
||||
// Owners and resharers both see Send — the post is "on their account"
|
||||
@@ -71,6 +79,32 @@ export function PostActionsFooter({ post }: { post: Post }) {
|
||||
await fn();
|
||||
}
|
||||
|
||||
// Like toggle — optimistic flip + count nudge, reconciled to the server's
|
||||
// effective total (DB count + pending Redis delta) on success, rolled back
|
||||
// on error. A ref guard ignores taps while a request is in flight.
|
||||
async function onLikeToggle() {
|
||||
if (likeBusy.current) return;
|
||||
likeBusy.current = true;
|
||||
const next = !liked;
|
||||
setLiked(next);
|
||||
setLikeCount((c) => Math.max(0, c + (next ? 1 : -1)));
|
||||
try {
|
||||
const res = next
|
||||
? await postsApi.like(post.id)
|
||||
: await postsApi.unlike(post.id);
|
||||
setLiked(res.liked);
|
||||
setLikeCount(res.likes_count);
|
||||
} catch (err) {
|
||||
setLiked(!next);
|
||||
setLikeCount((c) => Math.max(0, c + (next ? -1 : 1)));
|
||||
const msg =
|
||||
err instanceof SocialError ? err.message : "Could not update like";
|
||||
toast.error(msg, { duration: 3000 });
|
||||
} finally {
|
||||
likeBusy.current = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onReshare() {
|
||||
// Optimistic flip first so the UI feels instant; revert on error.
|
||||
setReshared(true);
|
||||
@@ -105,14 +139,26 @@ export function PostActionsFooter({ post }: { post: Post }) {
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<ActionButton
|
||||
label="Like"
|
||||
onClick={() =>
|
||||
withAuth(() => {
|
||||
toast("Likes are coming soon", { duration: 2000 });
|
||||
})
|
||||
label={liked ? "Unlike" : "Like"}
|
||||
onClick={() => withAuth(onLikeToggle)}
|
||||
icon={
|
||||
liked ? (
|
||||
<HiHeart className={cn(ICON_CLASS, "text-red-500")} />
|
||||
) : (
|
||||
<HiOutlineHeart className={ICON_CLASS} />
|
||||
)
|
||||
}
|
||||
icon={<HiOutlineHeart className={ICON_CLASS} />}
|
||||
/>
|
||||
{likeCount > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setLikesOpen(true)}
|
||||
aria-label={`${likeCount} ${likeCount === 1 ? "like" : "likes"}`}
|
||||
className="-ml-1 mr-1 rounded px-1 text-sm font-medium tabular-nums text-muted-foreground transition-colors hover:text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{likeCount}
|
||||
</button>
|
||||
) : null}
|
||||
<ActionButton
|
||||
label="Comment"
|
||||
onClick={() =>
|
||||
@@ -169,6 +215,11 @@ export function PostActionsFooter({ post }: { post: Post }) {
|
||||
isMobile={isMobile}
|
||||
existingPostId={post.id}
|
||||
/>
|
||||
<PostLikesDrawer
|
||||
open={likesOpen}
|
||||
postId={post.id}
|
||||
onClose={() => setLikesOpen(false)}
|
||||
/>
|
||||
{/* SharePostSheet is mounted globally by SharePostProvider, so
|
||||
no per-card instance lives here. The provider's
|
||||
openShareSheet swaps the active post under the hood. */}
|
||||
|
||||
Reference in New Issue
Block a user