From e752de67e1874600130763851e6b10d9e533ec1a Mon Sep 17 00:00:00 2001 From: TerryM Date: Tue, 2 Jun 2026 11:12:26 +0800 Subject: [PATCH] fix(banner): preserve active language when navigating to post links Banner linkUrls come back from the API as unprefixed paths (e.g. /browse?post=123). Navigating to them directly dropped non-English viewers into the English version of the post. Localize both the rendered href and the SPA navigate target via stripLangPrefix + localizePath. --- src/components/FigmaBanner.tsx | 52 ++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/src/components/FigmaBanner.tsx b/src/components/FigmaBanner.tsx index 2cecf88..64e3921 100644 --- a/src/components/FigmaBanner.tsx +++ b/src/components/FigmaBanner.tsx @@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom"; import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api"; import { EASE_OUT } from "../motion"; import { langQuery, useI18n, type Lang } from "../i18n"; +import { localizePath, stripLangPrefix } from "../languageRoutes"; const FIGMA_ASSET_BASE = "/assets/ark-library/figma"; @@ -63,6 +64,20 @@ function internalPath(linkUrl: string): string | null { } } +/** + * Banner link URLs are stored unprefixed (e.g. `/browse?post=123`). When the + * viewer is on a non-English locale we must re-prefix them with the active + * language path (`/malay/browse?post=123`) so navigation doesn't drop into + * the English version of the post. + */ +function localizeLinkUrl(linkUrl: string, lang: Lang): string { + const path = internalPath(linkUrl); + if (!path) return linkUrl; + const url = new URL(path, window.location.origin); + const bare = stripLangPrefix(url.pathname); + return localizePath(bare, lang) + url.search + url.hash; +} + function toSlides(items: BannerApiItem[]): BannerSlide[] { return [...items] .sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)) @@ -272,33 +287,21 @@ export function FigmaBanner() { const path = internalPath(linkUrl); if (!path) return; event.preventDefault(); - navigate(path); + navigate(localizeLinkUrl(path, lang)); }; if (slides.length === 0) return null; - // Cap the dot indicator at 10 so a long banner list never overflows the phone - // width. With more slides we show a 10-dot window that follows the active - // slide; each dot still maps to its real slide index. - const maxDots = 10; - const dotWindowStart = - slides.length <= maxDots - ? 0 - : Math.min( - Math.max(activeIndex - Math.floor(maxDots / 2), 0), - slides.length - maxDots, - ); - + // Show every slide's dot. The row stays within the screen width and wraps to + // a second row (and beyond if needed) instead of overflowing horizontally. const pagination = hasMultiple ? ( -
- {slides - .slice(dotWindowStart, dotWindowStart + maxDots) - .map((slide, offset) => { - const index = dotWindowStart + offset; +
+
+ {slides.map((slide, index) => { const active = index === activeIndex; return (
) : null; @@ -381,7 +385,7 @@ export function FigmaBanner() { > {slide.linkUrl ? ( handleSlideClick(event, slide.linkUrl!)}