import { ChevronRight } from "lucide-react"; import { useLocation } from "react-router-dom"; import { useEffect, useRef, useState } from "react"; import { getJSON, itemsOrEmpty, type Category } from "../../api"; import { CategoryIcon } from "../../components/CategoryIcon"; import { FigmaBanner } from "../../components/FigmaBanner"; import { ComingSoonLatestUpdateRow, LatestUpdateRow, } from "../../components/LatestUpdateRow"; import { RecommendedCard } from "../../components/RecommendedCard"; import { SectionHeader } from "../../components/SectionHeader"; import { MessageBubble } from "../../components/messageStream/MessageBubble"; import { langQuery, useI18n } from "../../i18n"; import { sourceLanguageQuery } from "../../i18nLanguages"; import { popularTagParam } from "../../utils/popularTag"; import { cleanCategoryDisplayName } from "../../utils/categoryDisplay"; import { postToResource, type PostBackedResource, } from "../../utils/postResourceAdapter"; import type { Post } from "../../types/post"; const FIGMA_CATEGORY_ORDER = [ "project-ppt", "daily-class", "official-announcement", "academy-materials", "global-evangelism", "daily-poster", "community-tweets", "video-hub", "subsidy-policy", "how-to", "official-assets", "media-coverage", "academy-video", "general", ]; function figmaCategoryRank(category: Category): number { const index = FIGMA_CATEGORY_ORDER.indexOf(category.slug); return index === -1 ? FIGMA_CATEGORY_ORDER.length : index; } export function Home() { const { t, lang } = useI18n(); const { hash } = useLocation(); const [cats, setCats] = useState([]); const [rec, setRec] = useState([]); const [latest, setLatest] = useState([]); const [latestPosts, setLatestPosts] = useState([]); const [popular, setPopular] = useState([]); const [popularPosts, setPopularPosts] = useState([]); const [categoryUnavailableOpen, setCategoryUnavailableOpen] = useState(false); const [err, setErr] = useState(null); const recRowRef = useRef(null); const categoryRowRef = useRef(null); const [activeCategoryPage, setActiveCategoryPage] = useState(0); const [canScrollRec, setCanScrollRec] = useState(false); const [recScroll, setRecScroll] = useState({ ratio: 1, progress: 0 }); useEffect(() => { const langParam = encodeURIComponent(langQuery(lang)); const languageParam = encodeURIComponent(sourceLanguageQuery(lang)); const catQ = `?lang=${langParam}`; const postQ = `?lang=${langParam}&language=${languageParam}`; Promise.all([ getJSON(`/api/categories${catQ}`), getJSON<{ items: Post[] }>(`/api/posts/recommended${postQ}&limit=12`), getJSON<{ items: Post[] }>(`/api/posts/latest${postQ}&limit=5`), getJSON<{ items: Post[] }>( `/api/posts${postQ}&tag=${popularTagParam(lang)}&limit=5`, ).catch((): { items: Post[] } => ({ items: [] })), ]) .then(([c, r, l, p]) => { setCats(itemsOrEmpty(c)); setRec( itemsOrEmpty(r.items).map((post) => postToResource(post, lang, itemsOrEmpty(c)), ), ); const latestItems = itemsOrEmpty(l.items); setLatestPosts(latestItems); setLatest( latestItems.map((post) => postToResource(post, lang, itemsOrEmpty(c)), ), ); const popularItems = itemsOrEmpty(p.items); setPopularPosts(popularItems); setPopular( popularItems.map((post) => postToResource(post, lang, itemsOrEmpty(c)), ), ); }) .catch((e) => setErr(String(e))); }, [lang]); const iconKeyForResource = (r: PostBackedResource) => cats.find((c) => c.id === r.categoryId)?.iconKey ?? "folder"; const figmaOrderedCategories = [...cats].sort( (a, b) => figmaCategoryRank(a) - figmaCategoryRank(b), ); const categoryPages: Category[][] = []; for (let index = 0; index < figmaOrderedCategories.length; index += 9) { categoryPages.push(figmaOrderedCategories.slice(index, index + 9)); } const activeCategoryCount = categoryPages[activeCategoryPage]?.length ?? 0; const activeCategoryRows = Math.ceil(activeCategoryCount / 3); const mobileCategoryHeight = activeCategoryRows * 88 + Math.max(0, activeCategoryRows - 1) * 8; useEffect(() => { const row = categoryRowRef.current; if (!row) return; const update = () => { const width = row.clientWidth || 1; const next = Math.round(row.scrollLeft / width); setActiveCategoryPage((prev) => (prev === next ? prev : next)); }; update(); row.addEventListener("scroll", update, { passive: true }); return () => row.removeEventListener("scroll", update); }, [cats.length]); useEffect(() => { setActiveCategoryPage(0); categoryRowRef.current?.scrollTo({ left: 0 }); }, [lang]); useEffect(() => { const row = recRowRef.current; if (!row) { setCanScrollRec(false); return; } const update = () => { const overflow = row.scrollWidth > row.clientWidth + 1; setCanScrollRec(overflow); const ratio = overflow ? row.clientWidth / row.scrollWidth : 1; const maxScroll = Math.max(1, row.scrollWidth - row.clientWidth); const progress = overflow ? row.scrollLeft / maxScroll : 0; setRecScroll({ ratio: Math.min(1, Math.max(0.15, ratio)), progress: Math.min(1, Math.max(0, progress)), }); }; update(); const resizeObserver = new ResizeObserver(update); resizeObserver.observe(row); row.addEventListener("scroll", update, { passive: true }); return () => { resizeObserver.disconnect(); row.removeEventListener("scroll", update); }; }, [rec.length]); const scrollRec = (dir: 1 | -1) => { recRowRef.current?.scrollBy({ left: dir * 280, behavior: "smooth" }); }; useEffect(() => { if (!hash) return; const id = hash.slice(1); if (!id) return; const frame = window.requestAnimationFrame(() => { document.getElementById(id)?.scrollIntoView({ block: id === "latest" ? "center" : "start", }); }); return () => window.cancelAnimationFrame(frame); }, [hash, cats.length, rec.length, latest.length, popular.length]); const latestPlaceholderCount = Math.max(0, 5 - latest.length); const popularPlaceholderCount = Math.max(0, 5 - popular.length); const recommendedDotCount = rec.length; const activeRecommendedDot = recommendedDotCount > 0 ? Math.min( recommendedDotCount - 1, Math.round(recScroll.progress * (recommendedDotCount - 1)), ) : 0; if (err) { return (
{err}
); } return (
{categoryPages.map((page, pageIndex) => (
{page.map((c) => ( ))}
))}
{categoryPages.length > 1 ? (
{categoryPages.map((_, index) => (
) : null}
{figmaOrderedCategories.map((c) => ( ))}
{rec.map((r, index) => (
))}
{Array.from({ length: recommendedDotCount }).map((_, index) => (
{canScrollRec ? ( ) : null}
{latestPosts.slice(0, 5).map((post) => ( ))}
{latest.map((r) => ( ))} {Array.from({ length: latestPlaceholderCount }).map((_, index) => ( ))}
{categoryUnavailableOpen ? (
setCategoryUnavailableOpen(false)} >
event.stopPropagation()} >
{t("featureUnavailable")}

{t("featureUnavailableDesc")}

) : null}
); }