Skip to content

Commit 4d50ea1

Browse files
feat: create page to render topics and top threads of it
Signed-off-by: amanraj <raj.aman4001@gmail.com>
1 parent 053560b commit 4d50ea1

File tree

9 files changed

+667
-66
lines changed

9 files changed

+667
-66
lines changed

apps/server/src/db/schema/thread.schema.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import {
1010
import { topics } from "./topic.schema";
1111
import { users } from "./user.schema";
1212

13+
14+
15+
1316
export const threads = pgTable(
1417
"thread",
1518
{

apps/server/src/routers/threads.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { count, eq } from "drizzle-orm";
1+
import { count, eq ,sql, desc } from "drizzle-orm";
22
import type { FastifyInstance , FastifyRequest} from "fastify";
33
import {
44
createThreadSchema,

apps/server/src/routers/topics.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { count, eq } from "drizzle-orm";
22
import type { FastifyInstance, FastifyRequest } from "fastify";
33
import { DrizzleClient } from "@/db/index";
44
import { topics as topicsTable } from "@/db/schema/topic.schema";
5+
56
import {
67
createTopicSchema,
78
topicIdParamsSchema,
@@ -57,6 +58,52 @@ export async function topicRoutes(fastify: FastifyInstance) {
5758
}
5859
}
5960
);
61+
62+
63+
fastify.get(
64+
"/topics/:id",
65+
{
66+
preHandler: [authenticateUser, attachUser],
67+
schema: {
68+
params: {
69+
type: 'object',
70+
properties: {
71+
id: { type: 'string' }
72+
},
73+
required: ['id']
74+
}
75+
}
76+
},
77+
78+
async (request: FastifyRequest, reply) => {
79+
const params = topicIdParamsSchema.safeParse(request.params);
80+
if (!params.success)
81+
return reply
82+
.status(400)
83+
.send({ success: false, error: "Invalid topic id" });
84+
85+
try {
86+
const topic = await DrizzleClient.query.topics.findFirst({
87+
where: (t, { eq }) => eq(t.id, params.data.id),
88+
});
89+
90+
if (!topic)
91+
return reply
92+
.status(404)
93+
.send({ success: false, error: "Topic not found" });
94+
95+
return reply.status(200).send({
96+
success: true,
97+
topic,
98+
});
99+
} catch (error) {
100+
fastify.log.error({ err: error }, "Failed to fetch topic");
101+
return reply
102+
.status(500)
103+
.send({ success: false, error: "Failed to fetch topic" });
104+
}
105+
}
106+
);
60107

61108
fastify.post(
62109
"/topics",

apps/web/src/components/ui/header.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ const Header = ({ hideThemeToggle = false }: HeaderProps) => {
3131
Welcome, {user?.firstName || user?.username || user?.email}!
3232
</span>
3333

34-
<Link to={`/profile/${user?.username}`}>
34+
<Link to={
35+
user?.username ? `/profile/${user.username}` : "/my/profile"
36+
}>
3537
<Button
3638
variant="outline"
3739
className="neo-brutal-button border-primary text-primary bg-secondary hover:bg-secondary hover:text-black"
3840
>
39-
My Profile
41+
{user?.firstName }
4042
</Button>
4143
</Link>
4244
<Button

apps/web/src/data/mock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ export const mockUser = {
2525
totalPosts: 234,
2626
};
2727

28-
// --- Categories ---
29-
export const categories = [
28+
// --- topics ---
29+
export const topics = [
3030
{
3131
id: 1,
3232
name: "General Discussion",

apps/web/src/routes/home.tsx

Lines changed: 171 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,167 @@
11
import { Eye, MessageSquare } from "lucide-react";
22
import { Link } from "react-router";
33
import Footer from "@/components/ui/footer";
4+
5+
import { useEffect, useState } from "react";
46
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+
};
644

745
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+
8105
return (
9106
<div className="min-h-screen flex flex-col bg-background">
10107
<Header />
11108
<main className="mx-auto max-w-7xl px-4 py-6 sm:py-8 flex-1">
12-
{/* Categories Section */}
109+
{/* topics Section */}
13110
<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+
57165
{/* Recent Threads Section */}
58166
<section>
59167
<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() {
108216
</section>
109217

110218
{/* 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+
)}
127237
</main>
128238
<Footer />
129239
</div>

0 commit comments

Comments
 (0)