import { ChevronRight } from "lucide-react"; import { Link } 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 { langQuery, useI18n } from "../../i18n"; import { sourceLanguageQuery } from "../../i18nLanguages"; import { categoryCardLines } from "../../utils/categoryDisplay"; import { postToResource, type PostBackedResource, } from "../../utils/postResourceAdapter"; import type { Post } from "../../types/post"; export function Home() { const { t, lang } = useI18n(); const [cats, setCats] = useState([]); const [rec, setRec] = useState([]); const [latest, setLatest] = useState([]); 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=8`), ]) .then(([c, r, l]) => { setCats(itemsOrEmpty(c)); setRec( itemsOrEmpty(r.items).map((post) => postToResource(post, lang, itemsOrEmpty(c)), ), ); setLatest( itemsOrEmpty(l.items).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 categoryPages: Category[][] = []; for (let index = 0; index < cats.length; index += 9) { categoryPages.push(cats.slice(index, index + 9)); } 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" }); }; const latestPlaceholderCount = Math.max(0, 5 - latest.length); if (err) { return (
{err}
); } return (
{categoryPages.map((page, pageIndex) => (
{page.map((c) => (
{c.name}
))}
))}
{categoryPages.length > 1 ? (
{categoryPages.map((_, index) => (
) : null}
{cats.map((c) => { const { line1, line2 } = categoryCardLines(c.name); return (
{line1}
{line2 ? (
{line2}
) : null}
); })}
{rec.map((r, index) => (
))}
{canScrollRec ? ( ) : null}
{latest.map((r) => ( ))} {Array.from({ length: latestPlaceholderCount }).map((_, index) => ( ))}
); }