@ieltsrealtest/ui
Version:
Reusable UI components for IELTS Real Test platform, built with React and TypeScript.
133 lines (132 loc) • 8.93 kB
JavaScript
'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;