diff --git a/src/app/topics/[slug]/page.tsx b/src/app/topics/[slug]/page.tsx index a9936548..40a2d477 100644 --- a/src/app/topics/[slug]/page.tsx +++ b/src/app/topics/[slug]/page.tsx @@ -1,4 +1,6 @@ import type { Metadata } from 'next'; +import Link from 'next/link'; +import { TrendingUp, Hash } from 'lucide-react'; import TopicFeed from '@/components/social/TopicFeed'; interface TopicPageProps { @@ -14,13 +16,94 @@ export async function generateMetadata({ params }: TopicPageProps): Promise t.slug !== currentSlug); + + return ( + + ); +} + export default async function TopicPage({ params }: TopicPageProps) { const { slug } = await params; return (
-
- +
+
+ {/* Main feed */} + + + {/* Sidebar */} +
+ +
+
); diff --git a/src/components/social/TopicFeed.tsx b/src/components/social/TopicFeed.tsx index 81c06cd8..89f8ef36 100644 --- a/src/components/social/TopicFeed.tsx +++ b/src/components/social/TopicFeed.tsx @@ -1,8 +1,20 @@ 'use client'; import { useEffect, useRef } from 'react'; +import Link from 'next/link'; import Image from 'next/image'; -import { UserCircle, Heart, MessageCircle, TrendingUp, Clock, ArrowUp } from 'lucide-react'; +import { + UserCircle, + Heart, + MessageCircle, + TrendingUp, + Clock, + ArrowUp, + Zap, + RefreshCw, + Hash, + BookOpen, +} from 'lucide-react'; import { useTopicFeed, type SortOption } from '@/hooks/useTopicFeed'; import { getRelativeTime, formatFollowerCount } from '@/utils/socialUtils'; @@ -35,26 +47,38 @@ const SORT_OPTIONS: { value: SortOption; label: string; icon: React.ReactNode }[ interface SortBarProps { current: SortOption; onChange: (s: SortOption) => void; + postCount?: number; } -function SortBar({ current, onChange }: SortBarProps) { +function SortBar({ current, onChange, postCount }: SortBarProps) { return ( -
- {SORT_OPTIONS.map(({ value, label, icon }) => ( - - ))} +
+
+ {SORT_OPTIONS.map(({ value, label, icon }) => ( + + ))} +
+ {postCount !== undefined && ( + + {formatFollowerCount(postCount)} post{postCount !== 1 ? 's' : ''} + + )}
); } @@ -66,8 +90,20 @@ interface TopicFeedProps { } export default function TopicFeed({ slug }: TopicFeedProps) { - const { topic, posts, loading, loadingMore, hasMore, sort, setSort, loadMore, error } = - useTopicFeed(slug); + const { + topic, + posts, + loading, + loadingMore, + hasMore, + sort, + setSort, + loadMore, + retry, + toggleFollow, + followLoading, + error, + } = useTopicFeed(slug); const sentinelRef = useRef(null); // Infinite scroll via IntersectionObserver @@ -92,13 +128,45 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
+
) : topic ? ( <> -

#{topic.name}

- {topic.description && ( -

{topic.description}

- )} +
+
+ +
+

+ #{topic.name} +

+ {topic.description && ( +

+ {topic.description} +

+ )} +
+
+ + {/* Follow button */} + +
+
@@ -118,9 +186,7 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
{/* Sort bar */} -
- -
+ {/* Posts list */}
@@ -131,23 +197,48 @@ export default function TopicFeed({ slug }: TopicFeedProps) { {/* Error state */} {error && !loading && ( -
+

{error}

+
)} {/* Empty state */} {!loading && !error && posts.length === 0 && ( -
-

- No posts in this topic yet. Be the first to share! +

+ +

+ No posts yet in #{topic?.name ?? slug} +

+

+ Be the first to share your knowledge!

+ + Create a post +
)} {/* Post items */} {posts.map((post) => ( -
+
{post.authorAvatar ? (
- + {post.authorName} + + - ·
-

- {post.title} -

+ +

+ {post.title} +

+

+ {post.body} +

+ -

- {post.body} -

+ {/* Tags */} + {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + {/* Actions row */}
+ + {/* Tip button */} + +
diff --git a/src/hooks/useTopicFeed.ts b/src/hooks/useTopicFeed.ts index 6754608c..e1cc8aac 100644 --- a/src/hooks/useTopicFeed.ts +++ b/src/hooks/useTopicFeed.ts @@ -15,6 +15,9 @@ interface UseTopicFeedReturn { sort: SortOption; setSort: (s: SortOption) => void; loadMore: () => void; + retry: () => void; + toggleFollow: () => Promise; + followLoading: boolean; error: string | null; } @@ -27,6 +30,7 @@ export function useTopicFeed(slug: string): UseTopicFeedReturn { const [loadingMore, setLoadingMore] = useState(false); const [sort, setSort] = useState('latest'); const [error, setError] = useState(null); + const [followLoading, setFollowLoading] = useState(false); const fetchPosts = useCallback( async (nextCursor?: string, currentSort: SortOption = sort) => { @@ -69,7 +73,6 @@ export function useTopicFeed(slug: string): UseTopicFeedReturn { [slug, sort], ); - // Reset and reload when slug or sort changes useEffect(() => { setPosts([]); setCursor(undefined); @@ -82,9 +85,59 @@ export function useTopicFeed(slug: string): UseTopicFeedReturn { if (!loadingMore && hasMore && cursor) fetchPosts(cursor); }, [loadingMore, hasMore, cursor, fetchPosts]); - const handleSetSort = useCallback((s: SortOption) => { - setSort(s); - }, []); + const retry = useCallback(() => { + setPosts([]); + setCursor(undefined); + setHasMore(true); + fetchPosts(undefined, sort); + }, [fetchPosts, sort]); + + const toggleFollow = useCallback(async () => { + if (!topic || followLoading) return; + setFollowLoading(true); + const wasFollowing = topic.isFollowing; + // Optimistic update + setTopic((t) => + t + ? { + ...t, + isFollowing: !wasFollowing, + followerCount: t.followerCount + (wasFollowing ? -1 : 1), + } + : t, + ); + try { + await apiClient.post(`/api/topics/${slug}/follow`, { follow: !wasFollowing }); + } catch { + // Revert on failure + setTopic((t) => + t + ? { + ...t, + isFollowing: wasFollowing, + followerCount: t.followerCount + (wasFollowing ? 1 : -1), + } + : t, + ); + } finally { + setFollowLoading(false); + } + }, [topic, followLoading, slug]); + + const handleSetSort = useCallback((s: SortOption) => setSort(s), []); - return { topic, posts, loading, loadingMore, hasMore, sort, setSort: handleSetSort, loadMore, error }; + return { + topic, + posts, + loading, + loadingMore, + hasMore, + sort, + setSort: handleSetSort, + loadMore, + retry, + toggleFollow, + followLoading, + error, + }; } diff --git a/src/utils/socialUtils.ts b/src/utils/socialUtils.ts index 0f0628d8..74f80bcb 100644 --- a/src/utils/socialUtils.ts +++ b/src/utils/socialUtils.ts @@ -4,6 +4,7 @@ export interface Topic { description?: string; postCount: number; followerCount: number; + isFollowing?: boolean; } export interface TopicPost { @@ -17,6 +18,7 @@ export interface TopicPost { likes: number; commentCount: number; createdAt: Date; + tags?: string[]; } export interface Activity {