{t('home.loading')}
} {!loadingThreads && recentThreads.length === 0 && ({t('portal.empty_posts')}
)} {!loadingThreads && recentThreads.length > 0 && (import { useEffect, useMemo, useState } from 'react' import { Container } from 'react-bootstrap' import { Link } from 'react-router-dom' import { fetchPortalSummary } from '../api/client' import PortalTopicRow from '../components/PortalTopicRow' import { useTranslation } from 'react-i18next' import { useAuth } from '../context/AuthContext' export default function Home() { const [forums, setForums] = useState([]) const [threads, setThreads] = useState([]) const [stats, setStats] = useState({ threads: 0, posts: 0, users: 0 }) const [error, setError] = useState('') const [loadingForums, setLoadingForums] = useState(true) const [loadingThreads, setLoadingThreads] = useState(true) const [loadingStats, setLoadingStats] = useState(true) const [profile, setProfile] = useState(null) const { token, roles, email } = useAuth() const { t } = useTranslation() useEffect(() => { let active = true setLoadingForums(true) setLoadingThreads(true) setLoadingStats(true) setError('') fetchPortalSummary() .then((data) => { if (!active) return setForums(data?.forums || []) setThreads(data?.threads || []) setStats({ threads: data?.stats?.threads ?? 0, posts: data?.stats?.posts ?? 0, users: data?.stats?.users ?? 0, }) setProfile(data?.profile || null) }) .catch((err) => { if (!active) return setError(err.message) setForums([]) setThreads([]) setStats({ threads: 0, posts: 0, users: 0 }) setProfile(null) }) .finally(() => { if (!active) return setLoadingForums(false) setLoadingThreads(false) setLoadingStats(false) }) return () => { active = false } }, [token]) const getParentId = (forum) => { if (!forum.parent) return null if (typeof forum.parent === 'string') { return forum.parent.split('/').pop() } return forum.parent.id ?? null } const forumTree = useMemo(() => { const map = new Map() const roots = [] forums.forEach((forum) => { map.set(String(forum.id), { ...forum, children: [] }) }) forums.forEach((forum) => { const parentId = getParentId(forum) const node = map.get(String(forum.id)) if (parentId && map.has(String(parentId))) { map.get(String(parentId)).children.push(node) } else { roots.push(node) } }) const sortNodes = (nodes) => { nodes.sort((a, b) => { if (a.position !== b.position) return a.position - b.position return a.name.localeCompare(b.name) }) nodes.forEach((node) => sortNodes(node.children)) } const aggregateNodes = (node) => { if (!node.children?.length) { return { threads: node.threads_count ?? 0, views: node.views_count ?? 0, posts: node.posts_count ?? 0, last: node.last_post_at ? { at: node.last_post_at, node } : null, } } let threads = node.threads_count ?? 0 let views = node.views_count ?? 0 let posts = node.posts_count ?? 0 let last = node.last_post_at ? { at: node.last_post_at, node } : null node.children.forEach((child) => { const agg = aggregateNodes(child) threads += agg.threads views += agg.views posts += agg.posts if (agg.last && (!last || agg.last.at > last.at)) { last = agg.last } }) node.threads_count = threads node.views_count = views node.posts_count = posts if (last) { const source = last.node node.last_post_at = source.last_post_at node.last_post_user_id = source.last_post_user_id node.last_post_user_name = source.last_post_user_name node.last_post_user_rank_color = source.last_post_user_rank_color node.last_post_user_group_color = source.last_post_user_group_color } return { threads, views, posts, last } } sortNodes(roots) roots.forEach((root) => aggregateNodes(root)) return roots }, [forums]) const forumMap = useMemo(() => { const map = new Map() forums.forEach((forum) => { map.set(String(forum.id), forum) }) return map }, [forums]) const recentThreads = useMemo(() => { return [...threads] .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)) .slice(0, 12) }, [threads]) const roleLabel = useMemo(() => { if (!roles?.length) return t('portal.user_role_member') if (roles.includes('ROLE_ADMIN')) return t('portal.user_role_operator') if (roles.includes('ROLE_MODERATOR')) return t('portal.user_role_moderator') return t('portal.user_role_member') }, [roles, t]) const resolveForumName = (thread) => { if (!thread?.forum) return t('portal.unknown_forum') const parts = thread.forum.split('/') const id = parts[parts.length - 1] return forumMap.get(String(id))?.name || t('portal.unknown_forum') } const resolveForumId = (thread) => { if (!thread?.forum) return null const parts = thread.forum.split('/') return parts[parts.length - 1] || null } const renderTree = (nodes, depth = 0) => nodes.map((node) => (
{t('home.loading')}
} {!loadingThreads && recentThreads.length === 0 && ({t('portal.empty_posts')}
)} {!loadingThreads && recentThreads.length > 0 && ({error}
}