fix: route home view-all links

This commit is contained in:
TerryM
2026-05-28 16:49:30 +08:00
parent 4c15e01460
commit 5fec82dbba
6 changed files with 90 additions and 68 deletions

View File

@@ -5,6 +5,8 @@ import {
useState, useState,
type PointerEvent as ReactPointerEvent, type PointerEvent as ReactPointerEvent,
} from "react"; } from "react";
import { assetUrl, getJSON, itemsOrEmpty } from "../api";
import { langQuery, useI18n, type Lang } from "../i18n";
const FIGMA_ASSET_BASE = "/assets/ark-library/figma"; const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
@@ -17,42 +19,46 @@ type BannerSlide = {
mobile: string; mobile: string;
desktop: string; desktop: string;
alt: 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[] = [ type BannerApiResponse = {
{ items?: BannerApiItem[] | null;
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: "",
},
];
const AUTOPLAY_MS = 3000; const AUTOPLAY_MS = 3000;
const RESUME_AFTER_INTERACTION_MS = 8000; 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() { export function FigmaBanner() {
const slides = BANNER_SLIDES; const { lang } = useI18n();
const [slides, setSlides] = useState<BannerSlide[]>([]);
const scrollerRef = useRef<HTMLDivElement>(null); const scrollerRef = useRef<HTMLDivElement>(null);
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);
const [autoplayPaused, setAutoplayPaused] = useState(false); const [autoplayPaused, setAutoplayPaused] = useState(false);
@@ -65,6 +71,22 @@ export function FigmaBanner() {
} | null>(null); } | null>(null);
const hasMultiple = slides.length > 1; const hasMultiple = slides.length > 1;
useEffect(() => {
let cancelled = false;
setActiveIndex(0);
getJSON<BannerApiResponse>(`/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 goTo = useCallback((index: number, behavior: ScrollBehavior) => {
const scroller = scrollerRef.current; const scroller = scrollerRef.current;
if (!scroller) return; if (!scroller) return;
@@ -168,6 +190,8 @@ export function FigmaBanner() {
} }
}; };
if (slides.length === 0) return null;
const pagination = hasMultiple ? ( const pagination = hasMultiple ? (
<div <div
className="flex items-center justify-center gap-1.5 md:gap-2" className="flex items-center justify-center gap-1.5 md:gap-2"
@@ -213,14 +237,8 @@ export function FigmaBanner() {
aria-roledescription="carousel" aria-roledescription="carousel"
aria-label="ARK Library banner" aria-label="ARK Library banner"
> >
{slides.map((slide, index) => ( {slides.map((slide, index) => {
<div const image = (
key={slide.id}
className="relative w-full shrink-0 snap-start"
role="group"
aria-roledescription="slide"
aria-label={`${index + 1} / ${slides.length}`}
>
<picture className="block w-full overflow-hidden bg-black md:rounded-xl"> <picture className="block w-full overflow-hidden bg-black md:rounded-xl">
<source media="(max-width: 767px)" srcSet={slide.mobile} /> <source media="(max-width: 767px)" srcSet={slide.mobile} />
<img <img
@@ -234,8 +252,26 @@ export function FigmaBanner() {
draggable={false} draggable={false}
/> />
</picture> </picture>
);
return (
<div
key={slide.id}
className="relative w-full shrink-0 snap-start"
role="group"
aria-roledescription="slide"
aria-label={`${index + 1} / ${slides.length}`}
>
{slide.linkUrl ? (
<a href={slide.linkUrl} className="block" rel="noreferrer">
{image}
</a>
) : (
image
)}
</div> </div>
))} );
})}
</div> </div>
{hasMultiple ? ( {hasMultiple ? (

View File

@@ -19,11 +19,10 @@ export function MessageStream({ scope }: MessageStreamProps) {
const type = sp.get("type") || "all"; const type = sp.get("type") || "all";
const q = (sp.get("q") || "").trim(); const q = (sp.get("q") || "").trim();
const sort = sp.get("sort") || ""; const sort = sp.get("sort") || "";
const tag = sp.get("tag") || "";
const params = useMemo( const params = useMemo(
() => ({ scope, type, q, sort, tag, lang }), () => ({ scope, type, q, sort, lang }),
[scope, type, q, sort, tag, lang], [scope, type, q, sort, lang],
); );
const { items, isLoading, error, hasMore, loadMore, reset } = const { items, isLoading, error, hasMore, loadMore, reset } =

View File

@@ -16,7 +16,6 @@ export type PostStreamParams = {
language?: string; language?: string;
q?: string; q?: string;
sort?: string; sort?: string;
tag?: string;
lang: Lang; lang: Lang;
}; };
@@ -61,9 +60,6 @@ function filterMock(params: PostStreamParams): Post[] {
return false; return false;
const q = params.q?.trim().toLowerCase(); const q = params.q?.trim().toLowerCase();
if (params.language && p.language !== params.language) return false; 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) { if (q) {
const haystack = [ const haystack = [
p.text ?? "", 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.scope.kind === "category") sp.set("category", params.scope.slug);
if (params.type && params.type !== "all") sp.set("type", params.type); if (params.type && params.type !== "all") sp.set("type", params.type);
if (params.sort) sp.set("sort", params.sort); 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 (params.language) sp.set("language", sourceLanguageQuery(params.language));
if (cursor) sp.set("cursor", cursor); if (cursor) sp.set("cursor", cursor);
return `${q ? "/api/posts/search" : "/api/posts"}?${sp.toString()}`; return `${q ? "/api/posts/search" : "/api/posts"}?${sp.toString()}`;
@@ -171,7 +166,6 @@ export function usePostStream(params: PostStreamParams): PostStreamResult {
params.language, params.language,
params.q, params.q,
params.sort, params.sort,
params.tag,
params.lang, params.lang,
]); ]);

View File

@@ -4,7 +4,6 @@ import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { ArkLogoMark } from "../components/ArkLogoMark"; import { ArkLogoMark } from "../components/ArkLogoMark";
import { useI18n, type Lang } from "../i18n"; import { useI18n, type Lang } from "../i18n";
import { LANG_OPTIONS } from "../i18nLanguages"; import { LANG_OPTIONS } from "../i18nLanguages";
import { isPopularTag, popularTagParam } from "../utils/popularTag";
type PublicNavWhich = type PublicNavWhich =
| "home" | "home"
@@ -21,7 +20,6 @@ function navIsActive(
search: string, search: string,
hash: string, hash: string,
which: PublicNavWhich, which: PublicNavWhich,
currentLang: Lang,
): boolean { ): boolean {
const sp = new URLSearchParams(search); const sp = new URLSearchParams(search);
switch (which) { switch (which) {
@@ -39,9 +37,7 @@ function navIsActive(
case "browseRecommended": case "browseRecommended":
return pathname === "/official-recommendations"; return pathname === "/official-recommendations";
case "browsePopular": case "browsePopular":
return ( return pathname === "/browse" && sp.get("sort") === "popular";
pathname === "/browse" && isPopularTag(sp.get("tag") || "", currentLang)
);
case "favorites": case "favorites":
return ( return (
pathname === "/favorites" || (pathname === "/" && hash === "#favorites") pathname === "/favorites" || (pathname === "/" && hash === "#favorites")
@@ -277,10 +273,10 @@ export function PublicLayout() {
const nav = useNavigate(); const nav = useNavigate();
const na = (which: PublicNavWhich) => const na = (which: PublicNavWhich) =>
navIsActive(pathname, search, hash, which, lang); navIsActive(pathname, search, hash, which);
const isHome = pathname === "/"; const isHome = pathname === "/";
const footerInContentFlow = pathname === "/browse"; const footerInContentFlow = pathname === "/browse";
const popularHref = `/browse?tag=${popularTagParam(lang)}`; const popularHref = "/browse?sort=popular";
const goSearch = () => { const goSearch = () => {
const s = q.trim(); const s = q.trim();

View File

@@ -2,22 +2,20 @@ import { useSearchParams } from "react-router-dom";
import { MessageStream } from "../../components/messageStream/MessageStream"; import { MessageStream } from "../../components/messageStream/MessageStream";
import { SectionHeader } from "../../components/SectionHeader"; import { SectionHeader } from "../../components/SectionHeader";
import { useI18n } from "../../i18n"; import { useI18n } from "../../i18n";
import { isPopularTag } from "../../utils/popularTag";
export function Browse() { export function Browse() {
const { t, lang } = useI18n(); const { t } = useI18n();
const [sp] = useSearchParams(); const [sp] = useSearchParams();
const q = sp.get("q") || ""; const q = sp.get("q") || "";
const sort = sp.get("sort") || ""; const sort = sp.get("sort") || "";
const tag = sp.get("tag") || "";
const title = q const title = q
? `${t("search")}: ${q}` ? `${t("search")}: ${q}`
: isPopularTag(tag, lang)
? t("popular")
: sort === "latest" : sort === "latest"
? t("latest") ? t("latest")
: sort === "recommended" : sort === "recommended"
? t("official") ? t("official")
: sort === "popular"
? t("popular")
: t("all"); : t("all");
return ( return (
<section> <section>

View File

@@ -13,7 +13,6 @@ import { SectionHeader } from "../../components/SectionHeader";
import { MessageBubble } from "../../components/messageStream/MessageBubble"; import { MessageBubble } from "../../components/messageStream/MessageBubble";
import { langQuery, useI18n } from "../../i18n"; import { langQuery, useI18n } from "../../i18n";
import { sourceLanguageQuery } from "../../i18nLanguages"; import { sourceLanguageQuery } from "../../i18nLanguages";
import { popularTagParam } from "../../utils/popularTag";
import { cleanCategoryDisplayName } from "../../utils/categoryDisplay"; import { cleanCategoryDisplayName } from "../../utils/categoryDisplay";
import { import {
postToResource, postToResource,
@@ -68,9 +67,9 @@ export function Home() {
Promise.all([ Promise.all([
getJSON<Category[]>(`/api/categories${catQ}`), getJSON<Category[]>(`/api/categories${catQ}`),
getJSON<{ items: Post[] }>(`/api/posts/recommended${postQ}&limit=12`), 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[] }>( getJSON<{ items: Post[] }>(
`/api/posts${postQ}&tag=${popularTagParam(lang)}&limit=5`, `/api/posts${postQ}&sort=popular&limit=5`,
).catch((): { items: Post[] } => ({ items: [] })), ).catch((): { items: Post[] } => ({ items: [] })),
]) ])
.then(([c, r, l, p]) => { .then(([c, r, l, p]) => {
@@ -367,7 +366,7 @@ export function Home() {
<div className="px-4 md:px-0"> <div className="px-4 md:px-0">
<SectionHeader <SectionHeader
title={t("latestSection")} title={t("latestSection")}
viewAllTo="/browse?sort=latest" viewAllTo="/browse"
viewAllLabel={t("viewAll")} viewAllLabel={t("viewAll")}
/> />
</div> </div>
@@ -393,7 +392,7 @@ export function Home() {
<div className="px-4 md:px-0"> <div className="px-4 md:px-0">
<SectionHeader <SectionHeader
title={t("popularSection")} title={t("popularSection")}
viewAllTo={`/browse?tag=${popularTagParam(lang)}`} viewAllTo="/browse?sort=popular"
viewAllLabel={t("viewAll")} viewAllLabel={t("viewAll")}
/> />
</div> </div>