From fea6e1c93b8bd75c8fb8a6977735e7a4e4fd0805 Mon Sep 17 00:00:00 2001 From: TerryM Date: Thu, 28 May 2026 16:19:45 +0800 Subject: [PATCH] feat: add category and recommendations pages --- src/App.tsx | 7 ++ src/pages/Categories/index.tsx | 114 ++++++++++++++++++++ src/pages/OfficialRecommendations/index.tsx | 61 +++++++++++ 3 files changed, 182 insertions(+) create mode 100644 src/pages/Categories/index.tsx create mode 100644 src/pages/OfficialRecommendations/index.tsx diff --git a/src/App.tsx b/src/App.tsx index 8b975b3..14cfb01 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,8 @@ import { I18nProvider } from "./i18n"; import { PublicLayout } from "./layouts/PublicLayout"; import { Home } from "./pages/Home"; import { Browse } from "./pages/Browse"; +import { CategoriesPage } from "./pages/Categories"; +import { OfficialRecommendationsPage } from "./pages/OfficialRecommendations"; import { SearchPage } from "./pages/Search"; import { PostRedirect } from "./pages/PostRedirect"; import { AboutPage } from "./pages/About"; @@ -26,6 +28,11 @@ export default function App() { }> } /> } /> + } /> + } + /> } diff --git a/src/pages/Categories/index.tsx b/src/pages/Categories/index.tsx new file mode 100644 index 0000000..bf77f01 --- /dev/null +++ b/src/pages/Categories/index.tsx @@ -0,0 +1,114 @@ +import { useEffect, useState } from "react"; +import { getJSON, itemsOrEmpty, type Category } from "../../api"; +import { CategoryIcon } from "../../components/CategoryIcon"; +import { SectionHeader } from "../../components/SectionHeader"; +import { langQuery, useI18n } from "../../i18n"; +import { cleanCategoryDisplayName } from "../../utils/categoryDisplay"; + +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 CategoriesPage() { + const { t, lang } = useI18n(); + const [cats, setCats] = useState([]); + const [unavailableOpen, setUnavailableOpen] = useState(false); + const [err, setErr] = useState(null); + + useEffect(() => { + getJSON( + `/api/categories?lang=${encodeURIComponent(langQuery(lang))}`, + ) + .then((items) => + setCats( + itemsOrEmpty(items).sort( + (a, b) => figmaCategoryRank(a) - figmaCategoryRank(b), + ), + ), + ) + .catch((e) => setErr(String(e))); + }, [lang]); + + if (err) { + return ( +
+ {err} +
+ ); + } + + return ( +
+ +
+ {cats.map((category) => ( + + ))} +
+ + {unavailableOpen ? ( +
setUnavailableOpen(false)} + > +
event.stopPropagation()} + > +
+ {t("featureUnavailable")} +
+

+ {t("featureUnavailableDesc")} +

+ +
+
+ ) : null} +
+ ); +} diff --git a/src/pages/OfficialRecommendations/index.tsx b/src/pages/OfficialRecommendations/index.tsx new file mode 100644 index 0000000..5bda02f --- /dev/null +++ b/src/pages/OfficialRecommendations/index.tsx @@ -0,0 +1,61 @@ +import { useEffect, useState } from "react"; +import { getJSON, itemsOrEmpty, type Category } from "../../api"; +import { RecommendedCard } from "../../components/RecommendedCard"; +import { SectionHeader } from "../../components/SectionHeader"; +import { langQuery, useI18n } from "../../i18n"; +import { sourceLanguageQuery } from "../../i18nLanguages"; +import type { Post } from "../../types/post"; +import { + postToResource, + type PostBackedResource, +} from "../../utils/postResourceAdapter"; + +export function OfficialRecommendationsPage() { + const { t, lang } = useI18n(); + const [items, setItems] = useState([]); + const [err, setErr] = useState(null); + + useEffect(() => { + const langParam = encodeURIComponent(langQuery(lang)); + const languageParam = encodeURIComponent(sourceLanguageQuery(lang)); + Promise.all([ + getJSON(`/api/categories?lang=${langParam}`), + getJSON<{ items: Post[] }>( + `/api/posts/recommended?lang=${langParam}&language=${languageParam}&limit=100`, + ), + ]) + .then(([categories, recommended]) => { + const cats = itemsOrEmpty(categories); + setItems( + itemsOrEmpty(recommended.items).map((post) => + postToResource(post, lang, cats), + ), + ); + }) + .catch((e) => setErr(String(e))); + }, [lang]); + + if (err) { + return ( +
+ {err} +
+ ); + } + + return ( +
+ +
+ {items.map((item, index) => ( + + ))} +
+
+ ); +}