diff --git a/src/components/FigmaBanner.tsx b/src/components/FigmaBanner.tsx index 85ca6a3..35d03a4 100644 --- a/src/components/FigmaBanner.tsx +++ b/src/components/FigmaBanner.tsx @@ -5,6 +5,8 @@ import { useState, type PointerEvent as ReactPointerEvent, } from "react"; +import { assetUrl, getJSON, itemsOrEmpty } from "../api"; +import { langQuery, useI18n, type Lang } from "../i18n"; const FIGMA_ASSET_BASE = "/assets/ark-library/figma"; @@ -17,42 +19,46 @@ type BannerSlide = { mobile: string; desktop: string; alt: string; + linkUrl?: string; }; -const BANNERS_BASE = "/assets/ark-library/banners"; +type BannerApiItem = { + id: number | string; + imageUrl: string; + linkUrl?: string; + sortOrder?: number; +}; -const BANNER_SLIDES: BannerSlide[] = [ - { - id: "ark-banner-1", - mobile: `${BANNERS_BASE}/banner-1.png`, - desktop: `${BANNERS_BASE}/banner-1.png`, - alt: "", - }, - { - id: "ark-banner-2", - mobile: `${BANNERS_BASE}/banner-2.png`, - desktop: `${BANNERS_BASE}/banner-2.png`, - alt: "", - }, - { - id: "ark-banner-3", - mobile: `${BANNERS_BASE}/banner-3.png`, - desktop: `${BANNERS_BASE}/banner-3.png`, - alt: "", - }, - { - id: "ark-banner-4", - mobile: `${BANNERS_BASE}/banner-4.png`, - desktop: `${BANNERS_BASE}/banner-4.png`, - alt: "", - }, -]; +type BannerApiResponse = { + items?: BannerApiItem[] | null; +}; const AUTOPLAY_MS = 3000; const RESUME_AFTER_INTERACTION_MS = 8000; +function bannerLangParam(lang: Lang): string { + return langQuery(lang).toLowerCase(); +} + +function toSlides(items: BannerApiItem[]): BannerSlide[] { + return [...items] + .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)) + .filter((item) => item.imageUrl) + .map((item) => { + const imageUrl = assetUrl(item.imageUrl); + return { + id: String(item.id), + mobile: imageUrl, + desktop: imageUrl, + alt: "", + linkUrl: item.linkUrl || undefined, + }; + }); +} + export function FigmaBanner() { - const slides = BANNER_SLIDES; + const { lang } = useI18n(); + const [slides, setSlides] = useState([]); const scrollerRef = useRef(null); const [activeIndex, setActiveIndex] = useState(0); const [autoplayPaused, setAutoplayPaused] = useState(false); @@ -65,6 +71,22 @@ export function FigmaBanner() { } | null>(null); const hasMultiple = slides.length > 1; + useEffect(() => { + let cancelled = false; + setActiveIndex(0); + getJSON(`/api/banners?lang=${bannerLangParam(lang)}`) + .then((res) => { + if (cancelled) return; + setSlides(toSlides(itemsOrEmpty(res.items))); + }) + .catch(() => { + if (!cancelled) setSlides([]); + }); + return () => { + cancelled = true; + }; + }, [lang]); + const goTo = useCallback((index: number, behavior: ScrollBehavior) => { const scroller = scrollerRef.current; if (!scroller) return; @@ -168,6 +190,8 @@ export function FigmaBanner() { } }; + if (slides.length === 0) return null; + const pagination = hasMultiple ? (
- {slides.map((slide, index) => ( -
+ {slides.map((slide, index) => { + const image = ( -
- ))} + ); + + return ( +
+ {slide.linkUrl ? ( + + {image} + + ) : ( + image + )} +
+ ); + })}
{hasMultiple ? ( diff --git a/src/components/messageStream/MessageStream.tsx b/src/components/messageStream/MessageStream.tsx index 5a58b06..7944946 100644 --- a/src/components/messageStream/MessageStream.tsx +++ b/src/components/messageStream/MessageStream.tsx @@ -19,11 +19,10 @@ export function MessageStream({ scope }: MessageStreamProps) { const type = sp.get("type") || "all"; const q = (sp.get("q") || "").trim(); const sort = sp.get("sort") || ""; - const tag = sp.get("tag") || ""; const params = useMemo( - () => ({ scope, type, q, sort, tag, lang }), - [scope, type, q, sort, tag, lang], + () => ({ scope, type, q, sort, lang }), + [scope, type, q, sort, lang], ); const { items, isLoading, error, hasMore, loadMore, reset } = diff --git a/src/components/messageStream/hooks/usePostStream.ts b/src/components/messageStream/hooks/usePostStream.ts index c144b5e..3bfcf6a 100644 --- a/src/components/messageStream/hooks/usePostStream.ts +++ b/src/components/messageStream/hooks/usePostStream.ts @@ -16,7 +16,6 @@ export type PostStreamParams = { language?: string; q?: string; sort?: string; - tag?: string; lang: Lang; }; @@ -61,9 +60,6 @@ function filterMock(params: PostStreamParams): Post[] { return false; const q = params.q?.trim().toLowerCase(); if (params.language && p.language !== params.language) return false; - const tag = params.tag?.trim().toLowerCase(); - if (tag && !(p.tags ?? []).some((t) => t.toLowerCase() === tag)) - return false; if (q) { const haystack = [ p.text ?? "", @@ -93,7 +89,6 @@ function buildRealUrl(params: PostStreamParams, cursor?: string): string { if (params.scope.kind === "category") sp.set("category", params.scope.slug); if (params.type && params.type !== "all") sp.set("type", params.type); if (params.sort) sp.set("sort", params.sort); - if (params.tag) sp.set("tag", params.tag); if (params.language) sp.set("language", sourceLanguageQuery(params.language)); if (cursor) sp.set("cursor", cursor); return `${q ? "/api/posts/search" : "/api/posts"}?${sp.toString()}`; @@ -171,7 +166,6 @@ export function usePostStream(params: PostStreamParams): PostStreamResult { params.language, params.q, params.sort, - params.tag, params.lang, ]); diff --git a/src/layouts/PublicLayout.tsx b/src/layouts/PublicLayout.tsx index 07bd0b5..be43405 100644 --- a/src/layouts/PublicLayout.tsx +++ b/src/layouts/PublicLayout.tsx @@ -4,7 +4,6 @@ import { Link, Outlet, useLocation, useNavigate } from "react-router-dom"; import { ArkLogoMark } from "../components/ArkLogoMark"; import { useI18n, type Lang } from "../i18n"; import { LANG_OPTIONS } from "../i18nLanguages"; -import { isPopularTag, popularTagParam } from "../utils/popularTag"; type PublicNavWhich = | "home" @@ -21,7 +20,6 @@ function navIsActive( search: string, hash: string, which: PublicNavWhich, - currentLang: Lang, ): boolean { const sp = new URLSearchParams(search); switch (which) { @@ -39,9 +37,7 @@ function navIsActive( case "browseRecommended": return pathname === "/official-recommendations"; case "browsePopular": - return ( - pathname === "/browse" && isPopularTag(sp.get("tag") || "", currentLang) - ); + return pathname === "/browse" && sp.get("sort") === "popular"; case "favorites": return ( pathname === "/favorites" || (pathname === "/" && hash === "#favorites") @@ -277,10 +273,10 @@ export function PublicLayout() { const nav = useNavigate(); const na = (which: PublicNavWhich) => - navIsActive(pathname, search, hash, which, lang); + navIsActive(pathname, search, hash, which); const isHome = pathname === "/"; const footerInContentFlow = pathname === "/browse"; - const popularHref = `/browse?tag=${popularTagParam(lang)}`; + const popularHref = "/browse?sort=popular"; const goSearch = () => { const s = q.trim(); diff --git a/src/pages/Browse/index.tsx b/src/pages/Browse/index.tsx index 8806927..12275a8 100644 --- a/src/pages/Browse/index.tsx +++ b/src/pages/Browse/index.tsx @@ -2,22 +2,20 @@ import { useSearchParams } from "react-router-dom"; import { MessageStream } from "../../components/messageStream/MessageStream"; import { SectionHeader } from "../../components/SectionHeader"; import { useI18n } from "../../i18n"; -import { isPopularTag } from "../../utils/popularTag"; export function Browse() { - const { t, lang } = useI18n(); + const { t } = useI18n(); const [sp] = useSearchParams(); const q = sp.get("q") || ""; const sort = sp.get("sort") || ""; - const tag = sp.get("tag") || ""; const title = q ? `${t("search")}: ${q}` - : isPopularTag(tag, lang) - ? t("popular") - : sort === "latest" - ? t("latest") - : sort === "recommended" - ? t("official") + : sort === "latest" + ? t("latest") + : sort === "recommended" + ? t("official") + : sort === "popular" + ? t("popular") : t("all"); return (
diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 8d7b08f..2bc7e41 100644 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -13,7 +13,6 @@ 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, @@ -68,9 +67,9 @@ export function Home() { 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}&sort=latest&limit=5`), getJSON<{ items: Post[] }>( - `/api/posts${postQ}&tag=${popularTagParam(lang)}&limit=5`, + `/api/posts${postQ}&sort=popular&limit=5`, ).catch((): { items: Post[] } => ({ items: [] })), ]) .then(([c, r, l, p]) => { @@ -367,7 +366,7 @@ export function Home() {
@@ -393,7 +392,7 @@ export function Home() {