|
1 | 1 | import { Eye, MessageSquare } from "lucide-react"; |
2 | 2 | import { Link } from "react-router"; |
3 | 3 | import Footer from "@/components/ui/footer"; |
| 4 | + |
| 5 | +import { useEffect, useState } from "react"; |
4 | 6 | import Header from "@/components/ui/header"; |
5 | | -import { categories, homeRecentThreads } from "@/data/mock"; |
| 7 | +import { topics, homeRecentThreads } from "@/data/mock"; |
| 8 | +import Loader from "@/components/loader"; |
| 9 | + |
| 10 | + |
| 11 | +// Conceptual interface for Recent Threads |
| 12 | +interface Topic { |
| 13 | + id: string; |
| 14 | + topicName: string; |
| 15 | + topicDescription: string; |
| 16 | + // These are from your backend's GET /topics (must be included in the response) |
| 17 | + threadCount?: number; |
| 18 | + postCount?: number; |
| 19 | +} |
| 20 | + |
| 21 | +interface ForumStats { |
| 22 | + totalTopics: number; |
| 23 | + totalPosts: number; |
| 24 | + totalMembers: number; |
| 25 | +} |
| 26 | + |
| 27 | +// ================================================================= |
| 28 | +// 2. CONFIGURATION & HELPERS |
| 29 | +// ================================================================= |
| 30 | + |
| 31 | +const backendUrl = import.meta.env.VITE_BACKEND_API_URL || "http://localhost:3000"; |
| 32 | + |
| 33 | +// Helper for visual data not stored in the database |
| 34 | +const getTopicPresentation = (id: string) => { |
| 35 | + // Simple logic to assign an icon and color based on the ID for styling |
| 36 | + const colors = ["bg-blue-500", "bg-green-500", "bg-yellow-500", "bg-red-500"]; |
| 37 | + const icons = ["DEV", "ASK", "H", "GEN"]; |
| 38 | + const hash = id.length % 4; |
| 39 | + return { |
| 40 | + colorClass: colors[hash % colors.length], |
| 41 | + icon: icons[hash % icons.length], |
| 42 | + }; |
| 43 | +}; |
6 | 44 |
|
7 | 45 | export default function HomePage() { |
| 46 | + |
| 47 | + const [topics, setTopics] = useState<Topic[]>([]); |
| 48 | + const [stats, setStats] = useState<ForumStats | null>(null); |
| 49 | + const [loading, setLoading] = useState(true); |
| 50 | + const [error, setError] = useState<string | null>(null); |
| 51 | + |
| 52 | + useEffect(() => { |
| 53 | + const fetchAllData = async () => { |
| 54 | + setLoading(true); |
| 55 | + setError(null); |
| 56 | + |
| 57 | + const fetchTopics = async () => { |
| 58 | + const response = await fetch(`${backendUrl}/topics`, { credentials: "include" }); |
| 59 | + const data = await response.json(); |
| 60 | + if (!response.ok || !data.success) { |
| 61 | + throw new Error(data.error || "Failed to fetch topics."); |
| 62 | + } |
| 63 | + setTopics(data.data); |
| 64 | + }; |
| 65 | + |
| 66 | + const fetchStats = async () => { |
| 67 | + // Keep this hardcoded until you create the GET /stats backend endpoint |
| 68 | + setStats({ |
| 69 | + totalTopics: 616, |
| 70 | + totalPosts: 9700, |
| 71 | + totalMembers: 1200 |
| 72 | + }); |
| 73 | + }; |
| 74 | + |
| 75 | + try { |
| 76 | + // Execute fetches concurrently |
| 77 | + await Promise.all([fetchTopics(), fetchStats()]); |
| 78 | + } catch (err) { |
| 79 | + console.error(err); |
| 80 | + setError(err instanceof Error ? err.message : "An error occurred while loading the forum data."); |
| 81 | + } finally { |
| 82 | + setLoading(false); |
| 83 | + } |
| 84 | + }; |
| 85 | + |
| 86 | + fetchAllData(); |
| 87 | + }, []); |
| 88 | + |
| 89 | + if (loading) { |
| 90 | + return ( |
| 91 | + <div className="min-h-screen flex flex-col bg-background"> |
| 92 | + <Header /> |
| 93 | + <main className="flex-1 flex items-center justify-center"> |
| 94 | + <Loader /> |
| 95 | + </main> |
| 96 | + <Footer /> |
| 97 | + </div> |
| 98 | + ); |
| 99 | + } |
| 100 | + |
| 101 | + if (error) { |
| 102 | + return <div className="text-center p-12 text-lg text-red-600">{error}</div>; |
| 103 | + } |
| 104 | + |
8 | 105 | return ( |
9 | 106 | <div className="min-h-screen flex flex-col bg-background"> |
10 | 107 | <Header /> |
11 | 108 | <main className="mx-auto max-w-7xl px-4 py-6 sm:py-8 flex-1"> |
12 | | - {/* Categories Section */} |
| 109 | + {/* topics Section */} |
13 | 110 | <section className="mb-8 sm:mb-12"> |
14 | | - <h2 className="mb-4 sm:mb-6 font-bold text-2xl sm:text-3xl text-foreground"> |
15 | | - Categories |
16 | | - </h2> |
17 | | - <div className="grid gap-4 sm:gap-6 sm:grid-cols-2 lg:grid-cols-3"> |
18 | | - {categories.map((category) => ( |
19 | | - <Link |
20 | | - key={category.id} |
21 | | - to={`/category/${category.id}`} |
22 | | - className="group block" |
23 | | - > |
24 | | - <div className="border-4 border-border bg-card text-card-foreground p-4 sm:p-6 shadow-[8px_8px_0px_0px_var(--shadow-color)] transition-all hover:shadow-[4px_4px_0px_0px_var(--shadow-color)] hover:translate-x-[4px] hover:translate-y-[4px]"> |
25 | | - <div className="mb-3 sm:mb-4 flex items-center gap-3"> |
26 | | - <div |
27 | | - className={`flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center border-3 border-border ${category.color} text-xl sm:text-2xl flex-shrink-0`} |
28 | | - > |
29 | | - {category.icon} |
30 | | - </div> |
31 | | - <div className="flex-1 min-w-0"> |
32 | | - <h3 className="font-bold text-lg sm:text-xl truncate"> |
33 | | - {category.name} |
34 | | - </h3> |
35 | | - </div> |
36 | | - </div> |
37 | | - <p className="mb-3 sm:mb-4 text-sm sm:text-base text-muted-foreground leading-relaxed line-clamp-2"> |
38 | | - {category.description} |
39 | | - </p> |
40 | | - <div className="flex gap-3 sm:gap-4 text-xs sm:text-sm font-bold"> |
41 | | - <span className="flex items-center gap-1"> |
42 | | - <MessageSquare className="h-3 w-3 sm:h-4 sm:w-4 flex-shrink-0" /> |
43 | | - <span className="whitespace-nowrap"> |
44 | | - {category.topics} Topics |
45 | | - </span> |
46 | | - </span> |
47 | | - <span className="text-muted-foreground whitespace-nowrap"> |
48 | | - {category.posts} Posts |
49 | | - </span> |
50 | | - </div> |
51 | | - </div> |
52 | | - </Link> |
53 | | - ))} |
54 | | - </div> |
55 | | - </section> |
56 | | - |
| 111 | + <h2 className="mb-4 sm:mb-6 font-bold text-2xl sm:text-3xl text-foreground"> |
| 112 | + Topics |
| 113 | + </h2> |
| 114 | + <div className="grid gap-4 sm:gap-6 sm:grid-cols-2 lg:grid-cols-3"> |
| 115 | + {topics.map((topic) => { |
| 116 | + const presentation = getTopicPresentation(topic.id); |
| 117 | + return ( |
| 118 | + <Link |
| 119 | + key={topic.id} |
| 120 | + // *** ROUTE: Links to the threads list for this topic *** |
| 121 | + to={`/topic/${topic.id}`} |
| 122 | + className="group block" |
| 123 | + > |
| 124 | + <div className="border-4 border-border bg-card text-card-foreground p-4 sm:p-6 shadow-[8px_8px_0px_0px_var(--shadow-color)] transition-all hover:shadow-[4px_4px_0px_0px_var(--shadow-color)] hover:translate-x-[4px] hover:translate-y-[4px]"> |
| 125 | + <div className="mb-3 sm:mb-4 flex items-center gap-3"> |
| 126 | + <div |
| 127 | + className={`flex h-10 w-10 sm:h-12 sm:w-12 items-center justify-center border-3 border-border ${presentation.colorClass} text-xl sm:text-2xl flex-shrink-0`} |
| 128 | + > |
| 129 | + {presentation.icon} |
| 130 | + </div> |
| 131 | + <div className="flex-1 min-w-0"> |
| 132 | + <h3 className="font-bold text-lg sm:text-xl truncate"> |
| 133 | + {topic.topicName} |
| 134 | + </h3> |
| 135 | + </div> |
| 136 | + </div> |
| 137 | + <p className="mb-3 sm:mb-4 text-sm sm:text-base text-muted-foreground leading-relaxed line-clamp-2"> |
| 138 | + {topic.topicDescription} |
| 139 | + </p> |
| 140 | + <div className="flex gap-3 sm:gap-4 text-xs sm:text-sm font-bold"> |
| 141 | + <span className="flex items-center gap-1"> |
| 142 | + <MessageSquare className="h-3 w-3 sm:h-4 sm:w-4 flex-shrink-0" /> |
| 143 | + <span className="whitespace-nowrap"> |
| 144 | + {/* Uses data from the backend if available, otherwise 0 */} |
| 145 | + {topic.threadCount ?? 0} Threads |
| 146 | + </span> |
| 147 | + </span> |
| 148 | + <span className="text-muted-foreground whitespace-nowrap"> |
| 149 | + {/* Uses data from the backend if available, otherwise 0 */} |
| 150 | + {topic.postCount ?? 0} Posts |
| 151 | + </span> |
| 152 | + </div> |
| 153 | + </div> |
| 154 | + </Link> |
| 155 | + ); |
| 156 | + })} |
| 157 | + </div> |
| 158 | + {topics.length === 0 && !loading && ( |
| 159 | + <div className="text-center py-12 text-muted-foreground border-4 border-border border-dashed p-8"> |
| 160 | + <p className="text-lg">No topics have been created yet.</p> |
| 161 | + </div> |
| 162 | + )} |
| 163 | + </section> |
| 164 | + |
57 | 165 | {/* Recent Threads Section */} |
58 | 166 | <section> |
59 | 167 | <div className="mb-4 sm:mb-6 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"> |
@@ -108,22 +216,24 @@ export default function HomePage() { |
108 | 216 | </section> |
109 | 217 |
|
110 | 218 | {/* Stats Footer */} |
111 | | - <div className="mt-8 sm:mt-12 grid gap-3 sm:gap-4 sm:grid-cols-3"> |
112 | | - <div className="border-4 border-border bg-primary text-primary-foreground p-4 sm:p-6 shadow-[6px_6px_0px_0px_var(--shadow-color)]"> |
113 | | - <div className="font-bold text-3xl sm:text-4xl">616</div> |
114 | | - <div className="mt-1 font-bold text-xs sm:text-sm"> |
115 | | - TOTAL TOPICS |
116 | | - </div> |
117 | | - </div> |
118 | | - <div className="border-4 border-border bg-secondary text-secondary-foreground p-4 sm:p-6 shadow-[6px_6px_0px_0px_var(--shadow-color)]"> |
119 | | - <div className="font-bold text-3xl sm:text-4xl">9.7K</div> |
120 | | - <div className="mt-1 font-bold text-xs sm:text-sm">TOTAL POSTS</div> |
121 | | - </div> |
122 | | - <div className="border-4 border-border bg-accent text-accent-foreground p-4 sm:p-6 shadow-[6px_6px_0px_0px_var(--shadow-color)]"> |
123 | | - <div className="font-bold text-3xl sm:text-4xl">1.2K</div> |
124 | | - <div className="mt-1 font-bold text-xs sm:text-sm">MEMBERS</div> |
125 | | - </div> |
126 | | - </div> |
| 219 | + {stats && ( |
| 220 | + <div className="mt-8 sm:mt-12 grid gap-3 sm:gap-4 sm:grid-cols-3"> |
| 221 | + <div className="border-4 border-border bg-primary text-primary-foreground p-4 sm:p-6 shadow-[6px_6px_0px_0px_var(--shadow-color)]"> |
| 222 | + <div className="font-bold text-3xl sm:text-4xl">{stats.totalTopics}</div> |
| 223 | + <div className="mt-1 font-bold text-xs sm:text-sm"> |
| 224 | + TOTAL TOPICS |
| 225 | + </div> |
| 226 | + </div> |
| 227 | + <div className="border-4 border-border bg-secondary text-secondary-foreground p-4 sm:p-6 shadow-[6px_6px_0px_0px_var(--shadow-color)]"> |
| 228 | + <div className="font-bold text-3xl sm:text-4xl">{stats.totalPosts}</div> |
| 229 | + <div className="mt-1 font-bold text-xs sm:text-sm">TOTAL POSTS</div> |
| 230 | + </div> |
| 231 | + <div className="border-4 border-border bg-accent text-accent-foreground p-4 sm:p-6 shadow-[6px_6px_0px_0px_var(--shadow-color)]"> |
| 232 | + <div className="font-bold text-3xl sm:text-4xl">{stats.totalMembers}</div> |
| 233 | + <div className="mt-1 font-bold text-xs sm:text-sm">MEMBERS</div> |
| 234 | + </div> |
| 235 | + </div> |
| 236 | + )} |
127 | 237 | </main> |
128 | 238 | <Footer /> |
129 | 239 | </div> |
|
0 commit comments