feat: support mobile video previews

This commit is contained in:
TerryM
2026-06-01 16:35:40 +08:00
parent c53032155b
commit a968f47640
16 changed files with 275 additions and 47 deletions

View File

@@ -11,7 +11,13 @@ import { DocumentMeta } from "../components/DocumentMeta";
import { SearchPanel } from "../components/SearchPanel";
import { useI18n, type Lang } from "../i18n";
import { LANG_OPTIONS } from "../i18nLanguages";
import { homePathForLang, isHomePathname } from "../languageRoutes";
import {
homePathForLang,
isHomePathname,
languageFromPathname,
stripLangPrefix,
} from "../languageRoutes";
import { useLocalizedPath } from "../useLocalizedPath";
type PublicNavWhich =
| "home"
@@ -29,25 +35,26 @@ function navIsActive(
which: PublicNavWhich,
): boolean {
const sp = new URLSearchParams(search);
const stripped = stripLangPrefix(pathname);
switch (which) {
case "home":
return isHomePathname(pathname);
case "browseAll":
return pathname === "/browse" && !sp.has("sort");
return stripped === "/browse" && !sp.has("sort");
case "categories":
return (
pathname === "/categories" ||
stripped === "/categories" ||
(isHomePathname(pathname) && hash === "#categories")
);
case "browseLatest":
return pathname === "/browse" && sp.get("sort") === "latest";
return stripped === "/browse" && sp.get("sort") === "latest";
case "browseRecommended":
return pathname === "/official-recommendations";
return stripped === "/official-recommendations";
case "browsePopular":
return pathname === "/browse" && sp.get("sort") === "popular";
return stripped === "/browse" && sp.get("sort") === "popular";
case "favorites":
return (
pathname === "/favorites" ||
stripped === "/favorites" ||
(isHomePathname(pathname) && hash === "#favorites")
);
default:
@@ -295,6 +302,15 @@ export function PublicLayout() {
const desktopSearchRef = useRef<HTMLDivElement>(null);
const desktopSearchPanelRef = useRef<HTMLDivElement>(null);
const nav = useNavigate();
const lp = useLocalizedPath();
// Keep i18n state in sync with URL so deep links (`/malay/browse`) flip the
// UI language even if the user navigated via address bar or shared link.
useEffect(() => {
const urlLang = languageFromPathname(pathname);
if (urlLang !== lang) setLang(urlLang);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
const na = (which: PublicNavWhich) =>
navIsActive(pathname, search, hash, which);
@@ -304,7 +320,7 @@ export function PublicLayout() {
setLang(nextLang);
if (isHome) nav(homePathForLang(nextLang), { replace: true });
};
const footerInContentFlow = pathname === "/browse";
const footerInContentFlow = stripLangPrefix(pathname) === "/browse";
// Current page name shown in the header brand slot (falls back to the brand).
const pageTitle = usePageTitle();
@@ -344,12 +360,12 @@ export function PublicLayout() {
if (idleId) ric.cancelIdleCallback?.(idleId);
};
}, [lang]);
const popularHref = "/browse?sort=popular";
const popularHref = lp("/browse?sort=popular");
const goSearch = () => {
const s = q.trim();
if (!s) return;
nav(`/browse?q=${encodeURIComponent(s)}`);
nav(lp(`/browse?q=${encodeURIComponent(s)}`));
setOpen(false);
setMobileSearchOpen(false);
setDesktopSearchOpen(false);
@@ -574,35 +590,35 @@ export function PublicLayout() {
aria-label={t("mainNav")}
>
<Link
to="/browse"
to={lp("/browse")}
className={navClassName(na("browseAll"))}
aria-current={na("browseAll") ? "page" : undefined}
>
{t("all")}
</Link>
<Link
to="/categories"
to={lp("/categories")}
className={navClassName(na("categories"))}
aria-current={na("categories") ? "page" : undefined}
>
{t("categories")}
</Link>
<Link
to="/official-recommendations"
to={lp("/official-recommendations")}
className={navClassName(na("browseRecommended"))}
aria-current={na("browseRecommended") ? "page" : undefined}
>
{t("official")}
</Link>
<Link
to="/browse?sort=latest"
to={lp("/browse?sort=latest")}
className={navClassName(na("browseLatest"))}
aria-current={na("browseLatest") ? "page" : undefined}
>
{t("latest")}
</Link>
<Link
to="/favorites"
to={lp("/favorites")}
className={navClassName(na("favorites"))}
aria-current={na("favorites") ? "page" : undefined}
>
@@ -661,7 +677,7 @@ export function PublicLayout() {
className={`${headerMenuAnimationClass} fixed inset-x-0 top-[64px] z-50 grid gap-2 bg-[#08070c] px-4 py-3 shadow-2xl shadow-black/50 min-[440px]:px-5 sm:px-6 md:top-[70px] md:px-9 min-[1000px]:hidden`}
>
<Link
to="/browse"
to={lp("/browse")}
className={mobileMenuNavClassName(na("browseAll"))}
aria-current={na("browseAll") ? "page" : undefined}
onClick={() => setOpen(false)}
@@ -669,7 +685,7 @@ export function PublicLayout() {
{t("all")}
</Link>
<Link
to="/categories"
to={lp("/categories")}
className={mobileMenuNavClassName(na("categories"))}
aria-current={na("categories") ? "page" : undefined}
onClick={() => setOpen(false)}
@@ -677,7 +693,7 @@ export function PublicLayout() {
{t("categories")}
</Link>
<Link
to="/official-recommendations"
to={lp("/official-recommendations")}
className={mobileMenuNavClassName(na("browseRecommended"))}
aria-current={na("browseRecommended") ? "page" : undefined}
onClick={() => setOpen(false)}
@@ -685,7 +701,7 @@ export function PublicLayout() {
{t("official")}
</Link>
<Link
to="/browse?sort=latest"
to={lp("/browse?sort=latest")}
className={mobileMenuNavClassName(na("browseLatest"))}
aria-current={na("browseLatest") ? "page" : undefined}
onClick={() => setOpen(false)}
@@ -693,7 +709,7 @@ export function PublicLayout() {
{t("latest")}
</Link>
<Link
to="/favorites"
to={lp("/favorites")}
className={mobileMenuNavClassName(na("favorites"))}
aria-current={na("favorites") ? "page" : undefined}
onClick={() => setOpen(false)}
@@ -767,15 +783,16 @@ export function PublicLayout() {
active={isHome}
/>
<BottomNavIcon
to="/browse"
to={lp("/browse")}
label={t("all")}
icon="document"
active={
pathname === "/browse" && !new URLSearchParams(search).get("sort")
stripLangPrefix(pathname) === "/browse" &&
!new URLSearchParams(search).get("sort")
}
/>
<BottomNavIcon
to="/favorites"
to={lp("/favorites")}
label={t("favorites")}
icon="bookmark"
active={na("favorites")}
@@ -789,7 +806,7 @@ export function PublicLayout() {
</div>
</nav>
{pathname === "/browse" ? <BackToTop /> : null}
{stripLangPrefix(pathname) === "/browse" ? <BackToTop /> : null}
</div>
);
}