this repo has no description
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: likes in feed

TurtlePaw 2988dc3c 9b286937

+61 -45
+2 -38
src/app/[did]/[uri]/page.tsx
··· 1 1 "use client"; 2 + import { LikeButton } from "@/components/LikeButton"; 2 3 import LikeCounter from "@/components/LikeCounter"; 3 4 import { SaveButton } from "@/components/SaveButton"; 4 5 import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; ··· 34 35 const [loading, setLoading] = useState(true); 35 36 const [post, setPost] = useState<PostView | null>(null); 36 37 const [error, setError] = useState<Error | null>(null); 37 - const [likeUri, setLikeUri] = useState<string | null>(null); 38 38 const { agent } = useAuth(); 39 39 40 40 useEffect(() => console.log("Agent", agent), [agent]); ··· 163 163 {/* Bottom Section - Stats and External Link */} 164 164 <div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4"> 165 165 <div className="flex flex-wrap gap-4 sm:gap-6"> 166 - <Button 167 - className="flex items-center gap-2 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5" 168 - variant={"ghost"} 169 - disabled={!post.viewer} 170 - onClick={async () => { 171 - if (!agent || !post.viewer) return; 172 - if (likeUri == null) { 173 - const uri = await agent.like(post.uri, post.cid); 174 - setLikeUri(uri.uri); 175 - } else { 176 - agent.deleteLike(likeUri); 177 - } 178 - 179 - //@ts-expect-error ignore this 180 - setPost((prev) => ({ 181 - ...prev, 182 - viewer: { 183 - ...prev!.viewer, 184 - like: !prev!.viewer?.like, 185 - }, 186 - likeCount: 187 - (prev!.likeCount || 0) + 188 - (prev!.viewer?.like ? -1 : 1), 189 - })); 190 - }} 191 - > 192 - <Heart 193 - className={clsx( 194 - "w-5 h-5", 195 - post.viewer?.like 196 - ? "fill-red-500 text-red-500" 197 - : "text-black/80 dark:text-white/80" 198 - )} 199 - /> 200 - <LikeCounter count={post.likeCount || 0} /> 201 - </Button> 202 - 166 + <LikeButton post={post} /> 203 167 <div className="flex items-center gap-2"> 204 168 <MessagesSquare className="w-5 h-5 text-black/80 dark:text-white/80" /> 205 169 <span className="text-sm font-medium text-black dark:text-white">
+14 -7
src/components/Feed.tsx
··· 17 17 import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 18 18 import { SaveButton } from "./SaveButton"; 19 19 import { UnsaveButton } from "./UnsaveButton"; 20 + import { LikeButton } from "./LikeButton"; 20 21 21 22 export type FeedItem = { 22 23 id: string; ··· 80 81 81 82 return ( 82 83 <div key={item.uri} className="relative group"> 83 - {ActionButton && ( 84 - <div className="absolute z-30 top-3 left-3 opacity-0 group-hover:opacity-100 transition-opacity"> 85 - <ActionButton image={index} post={item} /> 86 - </div> 87 - )} 84 + {/* Save/Unsave button – top-left */} 85 + <div className="absolute top-3 left-3 z-30 opacity-0 group-hover:opacity-100 transition-opacity"> 86 + {ActionButton && <ActionButton image={index} post={item} />} 87 + </div> 88 + 89 + {/* Like button – top-right */} 90 + <div className="absolute top-3 right-3 z-30 opacity-0 group-hover:opacity-100 transition-opacity"> 91 + <LikeButton post={item} /> 92 + </div> 93 + 94 + {/* Link wraps image only */} 88 95 <Link 89 96 href={`/${item.author.did}/${AtUri.make(item.uri).rkey}`} 90 97 className="block" ··· 106 113 className="object-cover filter blur-xl scale-110 opacity-30" 107 114 /> 108 115 109 - {/* Centered foreground image */} 116 + {/* Foreground image */} 110 117 <div className="relative z-10 flex items-center justify-center w-full min-h-[120px]"> 111 118 <Image 112 119 src={image.fullsize} ··· 119 126 /> 120 127 </div> 121 128 122 - {/* Author info and text overlay (only if author exists) */} 129 + {/* Author info */} 123 130 {item.author && ( 124 131 <div className="absolute inset-0 z-20 bg-black/40 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300 flex flex-col justify-between p-3"> 125 132 <div className="w-fit self-start" />
+45
src/components/LikeButton.tsx
··· 1 + import { useAuth } from "@/lib/useAuth"; 2 + import { Button } from "./ui/button"; 3 + import { PostView } from "@atproto/api/dist/client/types/app/bsky/feed/defs"; 4 + import { useState } from "react"; 5 + import { Heart } from "lucide-react"; 6 + import clsx from "clsx"; 7 + import LikeCounter from "./LikeCounter"; 8 + 9 + export function LikeButton({ post }: { post: PostView }) { 10 + const { agent } = useAuth(); 11 + const [likeUri, setLikeUri] = useState<string | null>(null); 12 + const [likes, setLikes] = useState(post.likeCount ?? 0); 13 + const [isLiked, setLiked] = useState(post.viewer?.like ?? false); 14 + return ( 15 + <Button 16 + className="flex items-center gap-2 cursor-pointer hover:bg-black/5 dark:hover:bg-white/5" 17 + variant={"ghost"} 18 + disabled={!post.viewer} 19 + onClick={async () => { 20 + if (!agent || !post.viewer) return; 21 + if (likeUri == null) { 22 + setLiked(true); 23 + setLikes(likes + 1); 24 + const uri = await agent.like(post.uri, post.cid); 25 + setLikeUri(uri.uri); 26 + } else { 27 + setLiked(false); 28 + setLikes(likes - 1); 29 + setLikeUri(null); 30 + await agent.deleteLike(likeUri); 31 + } 32 + }} 33 + > 34 + <Heart 35 + className={clsx( 36 + "w-5 h-5", 37 + isLiked 38 + ? "fill-red-500 text-red-500" 39 + : "text-black/80 dark:text-white/80" 40 + )} 41 + /> 42 + <LikeCounter count={likes || 0} /> 43 + </Button> 44 + ); 45 + }