Skip to content

Commit c9f54d4

Browse files
authored
Merge pull request #50 from codeunia-dev/feat/likefunctiontoblog
Feature: Add like button functionality to blog posts with tooltip support
2 parents 352ca4a + 6f27eb9 commit c9f54d4

File tree

2 files changed

+162
-3
lines changed

2 files changed

+162
-3
lines changed

app/api/blog/[slug]/like/route.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { createClient } from '@/lib/supabase/server';
3+
4+
// Helper to get current user
5+
async function getUser() {
6+
const supabase = await createClient();
7+
const { data: { user } } = await supabase.auth.getUser();
8+
return user;
9+
}
10+
11+
// Helper to extract slug from the request URL
12+
function getSlugFromRequest(req: NextRequest): string | null {
13+
// /api/blog/[slug]/like
14+
const url = req.nextUrl || new URL(req.url);
15+
const match = url.pathname.match(/\/blog\/([^/]+)\/like/);
16+
return match ? decodeURIComponent(match[1]) : null;
17+
}
18+
19+
export async function GET(req: NextRequest) {
20+
const supabase = await createClient();
21+
const blog_slug = getSlugFromRequest(req);
22+
if (!blog_slug) {
23+
return NextResponse.json({ error: 'Missing slug' }, { status: 400 });
24+
}
25+
// Get like count
26+
const { count, error: countError } = await supabase
27+
.from('blog_likes')
28+
.select('*', { count: 'exact', head: true })
29+
.eq('blog_slug', blog_slug);
30+
31+
// Get current user
32+
const user = await getUser();
33+
let likedByUser = false;
34+
if (user) {
35+
const { data: userLike } = await supabase
36+
.from('blog_likes')
37+
.select('id')
38+
.eq('blog_slug', blog_slug)
39+
.eq('user_id', user.id)
40+
.maybeSingle();
41+
likedByUser = !!userLike;
42+
}
43+
44+
if (countError) {
45+
return NextResponse.json({ error: countError.message }, { status: 500 });
46+
}
47+
return NextResponse.json({ count: count ?? 0, likedByUser });
48+
}
49+
50+
export async function POST(req: NextRequest) {
51+
const supabase = await createClient();
52+
const user = await getUser();
53+
if (!user) {
54+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
55+
}
56+
const blog_slug = getSlugFromRequest(req);
57+
if (!blog_slug) {
58+
return NextResponse.json({ error: 'Missing slug' }, { status: 400 });
59+
}
60+
// Insert like (ignore if already exists due to unique constraint)
61+
const { error } = await supabase
62+
.from('blog_likes')
63+
.insert({ user_id: user.id, blog_slug });
64+
if (error && !error.message.includes('duplicate key')) {
65+
return NextResponse.json({ error: error.message }, { status: 500 });
66+
}
67+
return NextResponse.json({ success: true });
68+
}
69+
70+
export async function DELETE(req: NextRequest) {
71+
const supabase = await createClient();
72+
const user = await getUser();
73+
if (!user) {
74+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
75+
}
76+
const blog_slug = getSlugFromRequest(req);
77+
if (!blog_slug) {
78+
return NextResponse.json({ error: 'Missing slug' }, { status: 400 });
79+
}
80+
const { error } = await supabase
81+
.from('blog_likes')
82+
.delete()
83+
.eq('user_id', user.id)
84+
.eq('blog_slug', blog_slug);
85+
if (error) {
86+
return NextResponse.json({ error: error.message }, { status: 500 });
87+
}
88+
return NextResponse.json({ success: true });
89+
}

app/blog/[slug]/page.tsx

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,82 @@ import { motion } from "framer-motion"
1212
import { BlogPost } from "@/components/data/blog-posts"
1313
import ReactMarkdown from 'react-markdown'
1414
import rehypeRaw from 'rehype-raw'
15+
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip"
1516

1617
import Header from "@/components/header";
1718
import Footer from "@/components/footer";
1819

20+
function LikeButton({ slug, isAuthenticated }: { slug: string, isAuthenticated: boolean }) {
21+
const [likeCount, setLikeCount] = useState(0);
22+
const [likedByUser, setLikedByUser] = useState(false);
23+
const [loading, setLoading] = useState(true);
24+
25+
useEffect(() => {
26+
async function fetchLikeData() {
27+
setLoading(true);
28+
try {
29+
const res = await fetch(`/api/blog/${slug}/like`);
30+
if (res.ok) {
31+
const data = await res.json();
32+
setLikeCount(data.count);
33+
setLikedByUser(data.likedByUser);
34+
}
35+
} catch {}
36+
setLoading(false);
37+
}
38+
if (slug) fetchLikeData();
39+
}, [slug]);
40+
41+
const handleLike = async () => {
42+
if (!isAuthenticated) {
43+
return;
44+
}
45+
setLoading(true);
46+
if (!likedByUser) {
47+
// Like the post
48+
const res = await fetch(`/api/blog/${slug}/like`, { method: "POST" });
49+
if (res.ok) {
50+
setLikeCount((c) => c + 1);
51+
setLikedByUser(true);
52+
}
53+
} else {
54+
// Unlike the post
55+
const res = await fetch(`/api/blog/${slug}/like`, { method: "DELETE" });
56+
if (res.ok) {
57+
setLikeCount((c) => c - 1);
58+
setLikedByUser(false);
59+
}
60+
}
61+
setLoading(false);
62+
};
63+
64+
return (
65+
<Tooltip>
66+
<TooltipTrigger asChild>
67+
<span>
68+
<Button
69+
onClick={handleLike}
70+
disabled={loading || !isAuthenticated}
71+
variant={likedByUser ? "default" : "ghost"}
72+
size="sm"
73+
className="flex items-center gap-2"
74+
>
75+
{likedByUser ? (
76+
<Heart className="h-4 w-4 text-red-500 fill-red-500" />
77+
) : (
78+
<Heart className="h-4 w-4" />
79+
)}
80+
<span>{likeCount}</span>
81+
</Button>
82+
</span>
83+
</TooltipTrigger>
84+
<TooltipContent sideOffset={8}>
85+
{isAuthenticated ? (likedByUser ? "Unlike" : "Like") : "Login to like posts"}
86+
</TooltipContent>
87+
</Tooltip>
88+
);
89+
}
90+
1991
export default function BlogPostPage() {
2092
const [isAuthenticated, setIsAuthenticated] = useState(false)
2193
const [isLoading, setIsLoading] = useState(true)
@@ -141,9 +213,7 @@ export default function BlogPostPage() {
141213
<Button variant="ghost" size="sm" className="hover:bg-primary/10 transition-colors">
142214
<Share2 className="h-4 w-4" />
143215
</Button>
144-
<Button variant="ghost" size="sm" className="hover:bg-primary/10 transition-colors">
145-
<Heart className="h-4 w-4" />
146-
</Button>
216+
<LikeButton slug={slug} isAuthenticated={isAuthenticated} />
147217
</motion.div>
148218
</div>
149219
</div>

0 commit comments

Comments
 (0)