feat: add post like

This commit is contained in:
gosangam
2026-06-09 16:34:10 +05:30
parent e36afdd1a5
commit 647a803b96
8 changed files with 441 additions and 266 deletions
+58 -7
View File
@@ -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. */}