UNPKG

@ieltsrealtest/ui

Version:

Reusable UI components for IELTS Real Test platform, built with React and TypeScript.

133 lines (132 loc) 8.93 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useCallback, useEffect, useMemo, useState } from 'react'; import CommentCard from './commentcard'; import CommentInput from './commentInput'; const COMMENTS_PER_PAGE = 5; const Discussions = ({ page, isDev = false }) => { const [discussions, setDiscussions] = useState([]); const [loading, setLoading] = useState(true); const [openThreads, setOpenThreads] = useState([]); const [activeReplyId, setActiveReplyId] = useState(null); const [visiblePages, setVisiblePages] = useState(1); const [userId, setUserId] = useState(''); const { USER_URL, BASE_URL } = useMemo(() => { return { USER_URL: isDev ? 'https://devapi.youready.net/ielts/user/api' : 'https://api.youready.net/ielts/user/api', BASE_URL: isDev ? 'https://devapi.youready.net/ielts/comment/api' : 'https://api.youready.net/ielts/comment/api', }; }, [isDev]); useEffect(() => { const fetchUser = async () => { try { const userRes = await fetch(`${USER_URL}/auth/get_user_id/`, { method: 'GET', credentials: 'include', headers: { 'Content-Type': 'application/json', }, }); const userData = await userRes.json(); if (!userRes.ok) { // window.location.href = `${process.env.NEXT_PUBLIC_SIGN_IN_URL}`; return; } setUserId(userData.user_id ?? ''); } catch (error) { // console.error('Failed to fetch user id:', error); } }; fetchUser(); }, []); const fetchDiscussions = useCallback(async () => { setLoading(true); try { const response = await fetch(`${BASE_URL}/discussion/?page=${page}`, { credentials: 'include', }); const json = await response.json(); setDiscussions(Array.isArray(json) ? json : []); } catch (error) { // console.error('Fetch error:', error); setDiscussions([]); } finally { setLoading(false); } }, [page]); useEffect(() => { setVisiblePages(1); setOpenThreads([]); setActiveReplyId(null); fetchDiscussions(); }, [fetchDiscussions]); const filteredDiscussions = useMemo(() => (Array.isArray(discussions) ? discussions : []), [discussions]); const grouped = useMemo(() => { return filteredDiscussions.reduce((acc, comment) => { if (!acc[comment.thread_id]) acc[comment.thread_id] = []; acc[comment.thread_id].push(comment); return acc; }, {}); }, [filteredDiscussions]); const mainComments = useMemo(() => { return filteredDiscussions .filter((comment) => comment.parent_id === null) .sort((a, b) => { if (a.pins && !b.pins) return -1; if (!a.pins && b.pins) return 1; return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); }); }, [filteredDiscussions]); const visibleMainComments = useMemo(() => { return mainComments.slice(0, visiblePages * COMMENTS_PER_PAGE); }, [mainComments, visiblePages]); const visibleThreadIds = useMemo(() => { return new Set(visibleMainComments.map((comment) => comment.thread_id)); }, [visibleMainComments]); const paginatedGrouped = useMemo(() => { return Object.entries(grouped) .sort(([, a], [, b]) => { const mainA = a.find((comment) => comment.parent_id === null); const mainB = b.find((comment) => comment.parent_id === null); const timeA = mainA ? new Date(mainA.created_at).getTime() : 0; const timeB = mainB ? new Date(mainB.created_at).getTime() : 0; return timeB - timeA; }) .filter(([threadId]) => visibleThreadIds.has(threadId)); }, [grouped, visibleThreadIds]); const totalMainComments = mainComments.length; const hasMore = visibleMainComments.length < totalMainComments; const toggleThreadReplies = (threadId) => { setOpenThreads((prev) => prev.includes(threadId) ? prev.filter((id) => id !== threadId) : [...prev, threadId]); }; const toggleReplyBox = (commentId) => { setActiveReplyId((prev) => (prev === commentId ? null : commentId)); }; if (loading) { return (_jsx("section", { className: "w-full max-w-3xl px-4 py-8 sm:px-6 lg:px-0", children: _jsx("p", { className: "text-sm text-gray-500", children: "Loading discussions..." }) })); } return (_jsxs("section", { className: "w-full px-4 py-8 sm:px-6 lg:px-0", children: [_jsxs("header", { className: "mb-8 flex items-baseline gap-2", children: [_jsx("h1", { className: "text-2xl font-semibold text-[#A11D33]", children: "Discussions" }), _jsxs("span", { className: "text-base font-medium text-gray-500", children: ["(", totalMainComments, ")"] })] }), userId && (_jsx("div", { className: "mb-10", children: _jsx(CommentInput, { userId: userId, onSuccess: fetchDiscussions, page: page, variant: "main", isDev: isDev }) })), paginatedGrouped.length === 0 ? (_jsx("div", { className: "rounded-lg border border-dashed border-gray-200 bg-gray-50 px-6 py-10 text-center text-sm text-gray-500", children: "No discussions yet. Be the first to start a conversation." })) : (_jsx("div", { className: "space-y-10", children: paginatedGrouped.map(([threadId, comments]) => { const mainComment = comments.find((comment) => comment.parent_id === null); if (!mainComment) return null; const replies = comments .filter((comment) => comment.parent_id !== null) .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); const isThreadOpen = openThreads.includes(threadId); const shouldToggleReplies = replies.length > 1; const visibleReplies = shouldToggleReplies ? isThreadOpen ? replies : replies.slice(0, 1) : replies; const replyCountLabel = `${replies.length} ${replies.length === 1 ? 'reply' : 'replies'}`; return (_jsxs("article", { className: "border-b border-gray-200 pb-10 last:border-b-0 last:pb-0", children: [_jsx(CommentCard, { id: mainComment._id, avatar: mainComment.avatar, name: mainComment.commenter, comment: mainComment.content, threadId: threadId, userId: userId, page: page, isReplying: activeReplyId === mainComment._id, onReplyClick: () => toggleReplyBox(mainComment._id), onRefresh: fetchDiscussions, role: mainComment.role, replied_user: '', time: mainComment.created_at, likes: mainComment.likes, pin: mainComment.pins, variant: "main" }), replies.length > 0 && (_jsxs("div", { className: "mt-5 pl-12", children: [_jsxs("div", { className: "relative", children: [visibleReplies.length > 0 && (_jsx("span", { className: "pointer-events-none absolute left-[-26px] top-0 w-px bg-gray-200", style: { height: '100%' }, "aria-hidden": "true" })), _jsx("div", { className: "space-y-6", children: visibleReplies.map((reply) => (_jsxs("div", { className: "relative", children: [_jsx("span", { className: "pointer-events-none absolute left-[-26px] top-6 w-6 border-t border-gray-200", "aria-hidden": "true" }), _jsx(CommentCard, { id: reply._id, avatar: reply.avatar, name: reply.commenter, comment: reply.content, threadId: threadId, page: page, userId: userId, isReplying: activeReplyId === reply._id, onReplyClick: () => toggleReplyBox(reply._id), onRefresh: fetchDiscussions, role: reply.role, replied_user: reply.replied_user || '', time: reply.created_at, likes: reply.likes, pin: reply.pins, variant: "reply" })] }, reply._id))) })] }), shouldToggleReplies && (_jsxs("button", { type: "button", onClick: () => toggleThreadReplies(threadId), className: "mt-4 flex items-center gap-2 text-sm font-medium text-[#1D4ED8] hover:text-[#153EAD]", children: [_jsx("span", { className: "text-xs", children: isThreadOpen ? '^' : 'v' }), replyCountLabel] }))] }))] }, threadId)); }) })), hasMore && (_jsx("div", { className: "mt-10 flex justify-center", children: _jsx("button", { type: "button", onClick: () => setVisiblePages((prev) => prev + 1), className: "rounded-full border border-[#DA1E37] px-6 py-2 text-sm font-semibold text-[#DA1E37] transition-colors hover:bg-[#DA1E37] hover:text-white", children: "See more" }) }))] })); }; export default Discussions;