From f54f237dd981b322de0bf5c73d94566f01704d68 Mon Sep 17 00:00:00 2001 From: Thibault Pouch Date: Tue, 3 Mar 2026 09:48:07 +0100 Subject: [PATCH] feat: integrate API calls for forum, bug, and event pages; replace mock data with dynamic data fetching --- nest-front/src/pages/public/AccountPage.tsx | 49 +++-- nest-front/src/pages/public/BugDetailPage.tsx | 97 +++++---- nest-front/src/pages/public/BugReportPage.tsx | 199 ++++++++++-------- nest-front/src/pages/public/EventsPage.tsx | 103 ++++----- nest-front/src/pages/public/ForumPage.tsx | 68 ++++-- nest-front/src/pages/public/StudioPage.tsx | 14 +- nest-front/src/pages/public/ThreadPage.tsx | 74 +++++-- 7 files changed, 363 insertions(+), 241 deletions(-) diff --git a/nest-front/src/pages/public/AccountPage.tsx b/nest-front/src/pages/public/AccountPage.tsx index 6f89be0..ac11370 100644 --- a/nest-front/src/pages/public/AccountPage.tsx +++ b/nest-front/src/pages/public/AccountPage.tsx @@ -1,17 +1,29 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { useAuth } from '../../contexts/AuthContext'; -import { MOCK_THREADS, MOCK_BUGS } from '../../data/mockData'; +import { bugsApi, forumApi, usersApi } from '../../utils/api'; import { formatDate } from '../../utils/format'; import { Link } from 'react-router-dom'; +import type { BugReport, ForumThread } from '../../types'; type Tab = 'profile' | 'threads' | 'bugs' | 'password'; export default function AccountPage() { const { user, updateUsername } = useAuth(); const [activeTab, setActiveTab] = useState('profile'); + const [userThreads, setUserThreads] = useState([]); + const [userBugs, setUserBugs] = useState([]); - const userThreads = MOCK_THREADS.filter((t) => t.authorId === user?.id); - const userBugs = MOCK_BUGS.filter((b) => b.submittedById === user?.id); + useEffect(() => { + if (!user) return; + + forumApi.getThreads({ limit: 200 }) + .then((res) => setUserThreads(res.data.filter((t) => t.authorId === user.id))) + .catch(() => setUserThreads([])); + + bugsApi.getBugs({ limit: 200 }) + .then((res) => setUserBugs(res.data.filter((b) => b.submittedById === user.id))) + .catch(() => setUserBugs([])); + }, [user]); const tabs: { id: Tab; label: string }[] = [ { id: 'profile', label: 'Profile' }, @@ -121,19 +133,23 @@ export default function AccountPage() { // ── Profile Tab ──────────────────────────────────────────────────────────────── -function ProfileTab({ user, updateUsername }: { user: NonNullable['user']>; updateUsername: (u: string) => void }) { +function ProfileTab({ user, updateUsername }: { user: NonNullable['user']>; updateUsername: (u: string) => Promise<{ success: boolean; error?: string }> }) { const [editing, setEditing] = useState(false); const [username, setUsername] = useState(user.username); const [error, setError] = useState(''); const [saved, setSaved] = useState(false); - const handleSave = useCallback(() => { + const handleSave = useCallback(async () => { if (!username.trim()) { setError('Username cannot be empty.'); return; } if (username.length < 3) { setError('Must be at least 3 characters.'); return; } - updateUsername(username.trim()); - setEditing(false); - setSaved(true); - setTimeout(() => setSaved(false), 3000); + const result = await updateUsername(username.trim()); + if (result.success) { + setEditing(false); + setSaved(true); + setTimeout(() => setSaved(false), 3000); + } else { + setError(result.error ?? 'Failed to update username.'); + } }, [username, updateUsername]); return ( @@ -210,10 +226,15 @@ function ChangePasswordForm() { if (Object.keys(next).length > 0) return; setLoading(true); - await new Promise((r) => setTimeout(r, 400)); - setLoading(false); - setForm({ current: '', next: '', confirm: '' }); - setErrors({ success: 'Password changed successfully.' }); + try { + await usersApi.changePassword(form.current, form.next); + setForm({ current: '', next: '', confirm: '' }); + setErrors({ success: 'Password changed successfully.' }); + } catch (err) { + setErrors({ current: err instanceof Error ? err.message : 'Failed to change password.' }); + } finally { + setLoading(false); + } }, [form]); return ( diff --git a/nest-front/src/pages/public/BugDetailPage.tsx b/nest-front/src/pages/public/BugDetailPage.tsx index 96417dc..9e03630 100644 --- a/nest-front/src/pages/public/BugDetailPage.tsx +++ b/nest-front/src/pages/public/BugDetailPage.tsx @@ -1,6 +1,6 @@ -import { useState, useCallback, useMemo } from 'react'; +import { useState, useCallback, useEffect, useMemo } from 'react'; import { Link, Navigate, useParams } from 'react-router-dom'; -import { MOCK_BUGS, MOCK_BUG_COMMENTS } from '../../data/mockData'; +import { bugsApi, ApiError } from '../../utils/api'; import { useAuth } from '../../contexts/AuthContext'; import { formatDate, formatDateTime } from '../../utils/format'; import type { BugComment, BugReport, BugSeverity, BugStatus } from '../../types'; @@ -57,21 +57,36 @@ export default function BugDetailPage() { const { id } = useParams<{ id: string }>(); const { user, isAuthenticated } = useAuth(); - // Local state — mirrors the global bug list in memory - const [bugs, setBugs] = useState(MOCK_BUGS); - const [comments, setComments] = useState(MOCK_BUG_COMMENTS); + const [bug, setBug] = useState(null); + const [comments, setComments] = useState([]); + const [loading, setLoading] = useState(true); + const [notFound, setNotFound] = useState(false); + const [newComment, setNewComment] = useState(''); const [commentError, setCommentError] = useState(''); const [submitting, setSubmitting] = useState(false); - const bug = useMemo(() => bugs.find((b) => b.id === id), [bugs, id]); + useEffect(() => { + if (!id) return; + let cancelled = false; + setLoading(true); - const bugComments = useMemo( - () => comments.filter((c) => c.bugReportId === id).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), - [comments, id] - ); + bugsApi.getBug(id) + .then((data) => { + if (cancelled) return; + const { comments: bugComments, ...bugData } = data; + setBug(bugData); + setComments(bugComments.sort((a, b) => a.createdAt.localeCompare(b.createdAt))); + }) + .catch((err) => { + if (cancelled) return; + if (err instanceof ApiError && err.status === 404) setNotFound(true); + }) + .finally(() => { if (!cancelled) setLoading(false); }); + + return () => { cancelled = true; }; + }, [id]); - // "I have this too" logic const alreadyVoted = useMemo( () => !!user && !!bug && bug.meTooBugs.includes(user.id), [user, bug] @@ -81,38 +96,44 @@ export default function BugDetailPage() { [user, bug] ); - const handleMeToo = useCallback(() => { + const handleMeToo = useCallback(async () => { if (!user || !bug || alreadyVoted || isOwnReport) return; - setBugs((prev) => - prev.map((b) => - b.id === bug.id ? { ...b, meTooBugs: [...b.meTooBugs, user.id] } : b - ) - ); + try { + await bugsApi.toggleMeToo(bug.id); + setBug((prev) => prev ? { ...prev, meTooBugs: [...prev.meTooBugs, user.id] } : prev); + } catch { + // silently ignore + } }, [user, bug, alreadyVoted, isOwnReport]); const handleComment = useCallback(async () => { - if (!user) return; + if (!user || !bug) return; if (!newComment.trim()) { setCommentError('Comment cannot be empty.'); return; } if (newComment.trim().length < 5) { setCommentError('Comment must be at least 5 characters.'); return; } setCommentError(''); setSubmitting(true); - await new Promise((r) => setTimeout(r, 250)); - const comment: BugComment = { - id: `bc${Date.now()}`, - bugReportId: id!, - authorId: user.id, - authorName: user.username, - content: newComment.trim(), - createdAt: new Date().toISOString(), - }; - setComments((prev) => [...prev, comment]); - setNewComment(''); - setSubmitting(false); - }, [user, newComment, id]); + try { + const comment = await bugsApi.addComment(bug.id, newComment.trim()); + setComments((prev) => [...prev, comment]); + setNewComment(''); + } catch (err) { + setCommentError(err instanceof Error ? err.message : 'Failed to post comment.'); + } finally { + setSubmitting(false); + } + }, [user, bug, newComment]); - if (!bug) { + if (loading) { + return ( +
+ Loading... +
+ ); + } + + if (notFound || !bug) { return ; } @@ -129,7 +150,6 @@ export default function BugDetailPage() { {/* Header */}
- {/* Badges */}
{bug.uniqueCode} @@ -138,7 +158,6 @@ export default function BugDetailPage() {
- {/* Title */}

- {/* Count */}
{metooCount}{' '} {metooCount === 1 ? 'user has' : 'users have'} this issue
- {/* Button logic */} {!isAuthenticated ? (
Login to confirm you have this issue @@ -285,22 +302,20 @@ export default function BugDetailPage() { Discussion - {bugComments.length} + {comments.length}
- {/* Comment list */} - {bugComments.length === 0 ? ( + {comments.length === 0 ? (
No comments yet. Be the first to comment.
) : ( - bugComments.map((comment) => ( + comments.map((comment) => ( )) )} - {/* Add comment */}
{isAuthenticated ? (
diff --git a/nest-front/src/pages/public/BugReportPage.tsx b/nest-front/src/pages/public/BugReportPage.tsx index 79caba8..bd78f6e 100644 --- a/nest-front/src/pages/public/BugReportPage.tsx +++ b/nest-front/src/pages/public/BugReportPage.tsx @@ -1,6 +1,6 @@ -import { useState, useMemo, useCallback } from 'react'; +import { useState, useMemo, useCallback, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { MOCK_BUGS } from '../../data/mockData'; +import { bugsApi } from '../../utils/api'; import { useAuth } from '../../contexts/AuthContext'; import { timeAgo } from '../../utils/format'; import type { BugReport, BugSeverity, BugStatus, BugReportFormData } from '../../types'; @@ -66,7 +66,6 @@ function BugCard({ bug, highlight }: BugCardProps) { - {/* MeToo count */} void }) { +function SubmitBugForm({ onSubmit }: { onSubmit: (data: BugReportFormData) => Promise }) { const [form, setForm] = useState({ title: '', description: '', @@ -128,6 +127,8 @@ function SubmitBugForm({ onSubmit }: { onSubmit: (data: BugReportFormData) => vo }); const [errors, setErrors] = useState>>({}); const [submitted, setSubmitted] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [submitError, setSubmitError] = useState(''); const set = useCallback((key: K, value: BugReportFormData[K]) => { setForm((prev) => ({ ...prev, [key]: value })); @@ -148,9 +149,16 @@ function SubmitBugForm({ onSubmit }: { onSubmit: (data: BugReportFormData) => vo const handleSubmit = useCallback(async (e: React.FormEvent) => { e.preventDefault(); if (!validate()) return; - await new Promise((r) => setTimeout(r, 400)); - onSubmit(form); - setSubmitted(true); + setSubmitting(true); + setSubmitError(''); + try { + await onSubmit(form); + setSubmitted(true); + } catch (err) { + setSubmitError(err instanceof Error ? err.message : 'Failed to submit report.'); + } finally { + setSubmitting(false); + } }, [form, onSubmit]); const labelStyle: React.CSSProperties = { @@ -186,6 +194,12 @@ function SubmitBugForm({ onSubmit }: { onSubmit: (data: BugReportFormData) => vo ▶ Submit a Bug Report
+ {submitError && ( +
+ [ERROR] {submitError} +
+ )} +
vo />
-
@@ -262,44 +276,37 @@ function SubmitBugForm({ onSubmit }: { onSubmit: (data: BugReportFormData) => vo export default function BugReportPage() { const { user, isAuthenticated } = useAuth(); - const [bugs, setBugs] = useState(MOCK_BUGS); + const [bugs, setBugs] = useState([]); + const [loading, setLoading] = useState(true); const [statusFilter, setStatusFilter] = useState('all'); const [severityFilter, setSeverityFilter] = useState('all'); const [showForm, setShowForm] = useState(false); - // Separate: user's own bugs and all others, both filtered + const fetchBugs = useCallback(() => { + setLoading(true); + bugsApi.getBugs({ status: statusFilter, severity: severityFilter, limit: 100 }) + .then((res) => setBugs(res.data)) + .catch(() => setBugs([])) + .finally(() => setLoading(false)); + }, [statusFilter, severityFilter]); + + useEffect(() => { fetchBugs(); }, [fetchBugs]); + const { myBugs, otherBugs } = useMemo(() => { - const passes = (b: BugReport) => { - if (statusFilter !== 'all' && b.status !== statusFilter) return false; - if (severityFilter !== 'all' && b.severity !== severityFilter) return false; - return true; - }; const my: BugReport[] = []; const other: BugReport[] = []; bugs.forEach((b) => { - if (!passes(b)) return; if (user && b.submittedById === user.id) my.push(b); else other.push(b); }); return { myBugs: my, otherBugs: other }; - }, [bugs, statusFilter, severityFilter, user]); + }, [bugs, user]); - const handleNewReport = useCallback((data: BugReportFormData) => { - const newBug: BugReport = { - id: `bug${Date.now()}`, - uniqueCode: `HH-${String(bugs.length + 1).padStart(4, '0')}`, - ...data, - status: 'open', - submittedById: user?.id ?? 'unknown', - submittedByName: user?.username ?? 'You', - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - notes: [], - meTooBugs: [], - }; + const handleNewReport = useCallback(async (data: BugReportFormData) => { + const newBug = await bugsApi.createBug(data); setBugs((prev) => [newBug, ...prev]); setShowForm(false); - }, [bugs.length, user]); + }, []); const openCount = bugs.filter((b) => b.status === 'open').length; const inProgressCount = bugs.filter((b) => b.status === 'in_progress').length; @@ -373,68 +380,78 @@ export default function BugReportPage() {

- {/* "Your Reports" section — only for logged-in users with their own bugs */} - {isAuthenticated && myBugs.length > 0 && ( -
-
- - ▶ Your Reports - - - {myBugs.length} - -
- {myBugs.map((bug) => ( - - ))} -
+ {loading && ( +
+ Loading reports... +
)} - {/* All other reports */} -
- {isAuthenticated && myBugs.length > 0 && ( -
- - All Reports - - - {otherBugs.length} - -
- )} + {!loading && ( + <> + {/* "Your Reports" section */} + {isAuthenticated && myBugs.length > 0 && ( +
+
+ + ▶ Your Reports + + + {myBugs.length} + +
+ {myBugs.map((bug) => ( + + ))} +
+ )} - {otherBugs.length === 0 && myBugs.length === 0 ? ( -
- No bug reports match the selected filters. -
- ) : otherBugs.length === 0 && isAuthenticated ? ( -
- No other reports match the selected filters. -
- ) : ( - otherBugs.map((bug) => ( - - )) - )} -
+ {/* All other reports */} +
+ {isAuthenticated && myBugs.length > 0 && ( +
+ + All Reports + + + {otherBugs.length} + +
+ )} + + {otherBugs.length === 0 && myBugs.length === 0 ? ( +
+ No bug reports match the selected filters. +
+ ) : otherBugs.length === 0 && isAuthenticated ? ( +
+ No other reports match the selected filters. +
+ ) : ( + otherBugs.map((bug) => ( + + )) + )} +
+ + )} ); } diff --git a/nest-front/src/pages/public/EventsPage.tsx b/nest-front/src/pages/public/EventsPage.tsx index cd2ae91..d7c297a 100644 --- a/nest-front/src/pages/public/EventsPage.tsx +++ b/nest-front/src/pages/public/EventsPage.tsx @@ -1,5 +1,5 @@ -import { useState, useCallback } from 'react'; -import { MOCK_EVENTS, MOCK_POLLS } from '../../data/mockData'; +import { useState, useCallback, useEffect } from 'react'; +import { eventsApi } from '../../utils/api'; import { useAuth } from '../../contexts/AuthContext'; import { formatDateTime } from '../../utils/format'; import type { EventPost, EventType, Poll, UserRole } from '../../types'; @@ -54,7 +54,7 @@ function PollCard({ poll, onVote }: { poll: Poll; onVote: (pollId: string, optio
{poll.options.map((option) => { const percentage = totalVotes > 0 ? Math.round((option.votes / totalVotes) * 100) : 0; - const userVoted = option.votedUserIds.includes(user?.id || ''); + const userVoted = option.votedUserIds.includes(user?.id ?? ''); return (
void; + onVote: (eventId: string, pollId: string, optionId: string) => void; }) { return (
{/* Poll if exists */} - {poll && } + {event.poll && ( + onVote(event.id, pollId, optionId)} + /> + )}
); } @@ -249,48 +252,28 @@ function EventCard({ export default function EventsPage() { const { user } = useAuth(); - // Filter to show only public events - const publicEvents = MOCK_EVENTS.filter((event) => event.isPublic); - const [events] = useState(publicEvents); - const [polls, setPolls] = useState(MOCK_POLLS); + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + eventsApi.getEvents(true) + .then((res) => setEvents(res.data)) + .catch(() => setEvents([])) + .finally(() => setLoading(false)); + }, []); const handleVote = useCallback( - (pollId: string, optionId: string) => { + async (eventId: string, _pollId: string, optionId: string) => { if (!user) return; - setPolls((prevPolls) => - prevPolls.map((poll) => { - if (poll.id !== pollId) return poll; - - const hasVotedForOption = poll.options.some((opt) => - opt.votedUserIds.includes(user.id) - ); - - return { - ...poll, - options: poll.options.map((opt) => { - if (opt.id === optionId) { - // Add vote to this option - return { - ...opt, - votes: opt.votedUserIds.includes(user.id) ? opt.votes : opt.votes + 1, - votedUserIds: opt.votedUserIds.includes(user.id) - ? opt.votedUserIds - : [...opt.votedUserIds, user.id], - }; - } else if (!poll.allowMultipleVotes && hasVotedForOption) { - // Remove vote from other options if single vote - return { - ...opt, - votes: opt.votedUserIds.includes(user.id) ? opt.votes - 1 : opt.votes, - votedUserIds: opt.votedUserIds.filter((id) => id !== user.id), - }; - } - return opt; - }), - }; - }) - ); + try { + const updatedEvent = await eventsApi.vote(eventId, [optionId]); + setEvents((prev) => + prev.map((e) => (e.id === updatedEvent.id ? updatedEvent : e)) + ); + } catch { + // silently ignore + } }, [user] ); @@ -334,7 +317,7 @@ export default function EventsPage() { {/* Events Grid */}
- {events.length === 0 ? ( + {loading ? (
-
+
+ Loading events... +
+
+ ) : events.length === 0 ? ( +
+
No events available at the moment. Check back soon!
) : ( - events.map((event) => { - const poll = event.pollId ? polls.find((p) => p.id === event.pollId) : undefined; - return ; - }) + events.map((event) => ( + + )) )}
diff --git a/nest-front/src/pages/public/ForumPage.tsx b/nest-front/src/pages/public/ForumPage.tsx index 9229038..530f5d2 100644 --- a/nest-front/src/pages/public/ForumPage.tsx +++ b/nest-front/src/pages/public/ForumPage.tsx @@ -1,6 +1,6 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { Link } from 'react-router-dom'; -import { MOCK_CATEGORIES, MOCK_THREADS } from '../../data/mockData'; +import { forumApi } from '../../utils/api'; import { timeAgo } from '../../utils/format'; import type { ForumCategory, ForumThread } from '../../types'; @@ -128,15 +128,43 @@ function CategoryCard({ category, threads }: { category: ForumCategory; threads: export default function ForumPage() { const [search, setSearch] = useState(''); + const [categories, setCategories] = useState([]); + const [threads, setThreads] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + let cancelled = false; + setLoading(true); + + Promise.all([ + forumApi.getCategories(), + forumApi.getThreads({ limit: 200 }), + ]) + .then(([cats, threadRes]) => { + if (cancelled) return; + setCategories(cats); + setThreads(threadRes.data); + }) + .catch(() => { + if (cancelled) return; + setError('Failed to load forum. Please try again.'); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { cancelled = true; }; + }, []); const filteredCategories = useMemo(() => { - if (!search.trim()) return MOCK_CATEGORIES; + if (!search.trim()) return categories; const q = search.toLowerCase(); - return MOCK_CATEGORIES.filter((cat) => + return categories.filter((cat) => cat.name.toLowerCase().includes(q) || - MOCK_THREADS.some((t) => t.categoryId === cat.id && t.title.toLowerCase().includes(q)) + threads.some((t) => t.categoryId === cat.id && t.title.toLowerCase().includes(q)) ); - }, [search]); + }, [search, categories, threads]); return (
@@ -173,15 +201,29 @@ export default function ForumPage() {
- {/* Categories */} - {filteredCategories.length === 0 ? ( + {loading && (
- No results found for "{search}" + Loading forum...
- ) : ( - filteredCategories.map((cat) => ( - - )) + )} + + {error && !loading && ( +
+ {error} +
+ )} + + {/* Categories */} + {!loading && !error && ( + filteredCategories.length === 0 ? ( +
+ No results found for "{search}" +
+ ) : ( + filteredCategories.map((cat) => ( + + )) + ) )}
); diff --git a/nest-front/src/pages/public/StudioPage.tsx b/nest-front/src/pages/public/StudioPage.tsx index b398088..55afe61 100644 --- a/nest-front/src/pages/public/StudioPage.tsx +++ b/nest-front/src/pages/public/StudioPage.tsx @@ -1,6 +1,16 @@ -import { TEAM_MEMBERS } from '../../data/mockData'; +import { useEffect, useState } from 'react'; +import { teamApi } from '../../utils/api'; +import type { TeamMember } from '../../types'; export default function StudioPage() { + const [members, setMembers] = useState([]); + + useEffect(() => { + teamApi.getMembers() + .then(setMembers) + .catch(() => { /* show empty state */ }); + }, []); + return (
{/* Header */} @@ -127,7 +137,7 @@ export default function StudioPage() { gap: '1.25rem', }} > - {TEAM_MEMBERS.map((member) => ( + {members.map((member) => (
{/* Avatar */} diff --git a/nest-front/src/pages/public/ThreadPage.tsx b/nest-front/src/pages/public/ThreadPage.tsx index 1c521ca..ed24d0e 100644 --- a/nest-front/src/pages/public/ThreadPage.tsx +++ b/nest-front/src/pages/public/ThreadPage.tsx @@ -1,23 +1,48 @@ -import { useState, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Link, useParams, Navigate } from 'react-router-dom'; -import { MOCK_THREADS, MOCK_REPLIES } from '../../data/mockData'; +import { forumApi, ApiError } from '../../utils/api'; import { useAuth } from '../../contexts/AuthContext'; import { formatDateTime, timeAgo } from '../../utils/format'; +import type { ForumThread, ForumReply } from '../../types'; export default function ThreadPage() { const { id } = useParams<{ id: string }>(); const { user, isAuthenticated } = useAuth(); - const thread = MOCK_THREADS.find((t) => t.id === id); + const [thread, setThread] = useState(null); + const [replies, setReplies] = useState([]); + const [loading, setLoading] = useState(true); + const [notFound, setNotFound] = useState(false); - // Local state for new reply (stored in memory, not persisted) - const [replies, setReplies] = useState( - MOCK_REPLIES.filter((r) => r.threadId === id) - ); const [newReply, setNewReply] = useState(''); const [replyError, setReplyError] = useState(''); const [submitting, setSubmitting] = useState(false); + useEffect(() => { + if (!id) return; + let cancelled = false; + setLoading(true); + + forumApi.getThread(id) + .then((data) => { + if (cancelled) return; + const { replies: threadReplies, ...threadData } = data; + setThread(threadData); + setReplies(threadReplies); + }) + .catch((err) => { + if (cancelled) return; + if (err instanceof ApiError && err.status === 404) { + setNotFound(true); + } + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { cancelled = true; }; + }, [id]); + const handleReply = useCallback(async () => { if (!newReply.trim()) { setReplyError('Reply cannot be empty.'); @@ -30,23 +55,26 @@ export default function ThreadPage() { setReplyError(''); setSubmitting(true); - await new Promise((r) => setTimeout(r, 300)); + try { + const reply = await forumApi.createReply(id!, newReply.trim()); + setReplies((prev) => [...prev, reply]); + setNewReply(''); + } catch (err) { + setReplyError(err instanceof Error ? err.message : 'Failed to post reply.'); + } finally { + setSubmitting(false); + } + }, [newReply, id]); - const reply = { - id: `r${Date.now()}`, - content: newReply.trim(), - authorId: user!.id, - authorName: user!.username, - threadId: id!, - createdAt: new Date().toISOString(), - }; + if (loading) { + return ( +
+ Loading thread... +
+ ); + } - setReplies((prev) => [...prev, reply]); - setNewReply(''); - setSubmitting(false); - }, [newReply, user, id]); - - if (!thread) { + if (notFound || !thread) { return ; } @@ -203,7 +231,7 @@ export default function ThreadPage() {