-
+
+
+ {/* 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 && (
-
+
)}
{/* 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 */}
@@ -194,6 +309,16 @@ export default function TopicFeed({ slug }: TopicFeedProps) {
{formatFollowerCount(post.commentCount)}
comments
+
+ {/* Tip button */}
+
+
+ Tip
+
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 {