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.
This commit is contained in:
@@ -11,6 +11,7 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
|
import { assetUrl, getJSON, itemsOrEmpty, readJSONCache } from "../api";
|
||||||
import { EASE_OUT } from "../motion";
|
import { EASE_OUT } from "../motion";
|
||||||
import { langQuery, useI18n, type Lang } from "../i18n";
|
import { langQuery, useI18n, type Lang } from "../i18n";
|
||||||
|
import { localizePath, stripLangPrefix } from "../languageRoutes";
|
||||||
|
|
||||||
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
|
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[] {
|
function toSlides(items: BannerApiItem[]): BannerSlide[] {
|
||||||
return [...items]
|
return [...items]
|
||||||
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
.sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0))
|
||||||
@@ -272,33 +287,21 @@ export function FigmaBanner() {
|
|||||||
const path = internalPath(linkUrl);
|
const path = internalPath(linkUrl);
|
||||||
if (!path) return;
|
if (!path) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
navigate(path);
|
navigate(localizeLinkUrl(path, lang));
|
||||||
};
|
};
|
||||||
|
|
||||||
if (slides.length === 0) return null;
|
if (slides.length === 0) return null;
|
||||||
|
|
||||||
// Cap the dot indicator at 10 so a long banner list never overflows the phone
|
// Show every slide's dot. The row stays within the screen width and wraps to
|
||||||
// width. With more slides we show a 10-dot window that follows the active
|
// a second row (and beyond if needed) instead of overflowing horizontally.
|
||||||
// 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,
|
|
||||||
);
|
|
||||||
|
|
||||||
const pagination = hasMultiple ? (
|
const pagination = hasMultiple ? (
|
||||||
<div
|
<div className="px-4">
|
||||||
className="flex max-w-full items-center justify-center gap-1.5 md:gap-2"
|
<div
|
||||||
role="tablist"
|
className="mx-auto flex max-w-full flex-wrap items-center justify-center gap-1.5 md:gap-2"
|
||||||
aria-label="Banner pagination"
|
role="tablist"
|
||||||
>
|
aria-label="Banner pagination"
|
||||||
{slides
|
>
|
||||||
.slice(dotWindowStart, dotWindowStart + maxDots)
|
{slides.map((slide, index) => {
|
||||||
.map((slide, offset) => {
|
|
||||||
const index = dotWindowStart + offset;
|
|
||||||
const active = index === activeIndex;
|
const active = index === activeIndex;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -312,7 +315,7 @@ export function FigmaBanner() {
|
|||||||
setActiveIndex(index);
|
setActiveIndex(index);
|
||||||
goTo(index, "smooth");
|
goTo(index, "smooth");
|
||||||
}}
|
}}
|
||||||
className={`h-1.5 rounded-full transition-all ${
|
className={`h-1.5 shrink-0 rounded-full transition-all ${
|
||||||
active
|
active
|
||||||
? "w-6 bg-ark-gold"
|
? "w-6 bg-ark-gold"
|
||||||
: "w-1.5 bg-[#7C7C7C] hover:bg-white/50"
|
: "w-1.5 bg-[#7C7C7C] hover:bg-white/50"
|
||||||
@@ -320,6 +323,7 @@ export function FigmaBanner() {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
@@ -381,7 +385,7 @@ export function FigmaBanner() {
|
|||||||
>
|
>
|
||||||
{slide.linkUrl ? (
|
{slide.linkUrl ? (
|
||||||
<a
|
<a
|
||||||
href={slide.linkUrl}
|
href={localizeLinkUrl(slide.linkUrl, lang)}
|
||||||
className="block"
|
className="block"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
onClick={(event) => handleSlideClick(event, slide.linkUrl!)}
|
onClick={(event) => handleSlideClick(event, slide.linkUrl!)}
|
||||||
|
|||||||
Reference in New Issue
Block a user