fix: route home view-all links
This commit is contained in:
@@ -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>
|
||||||
</div>
|
);
|
||||||
))}
|
|
||||||
|
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>
|
||||||
|
|
||||||
{hasMultiple ? (
|
{hasMultiple ? (
|
||||||
|
|||||||
@@ -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 } =
|
||||||
|
|||||||
@@ -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,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)
|
: sort === "latest"
|
||||||
? t("popular")
|
? t("latest")
|
||||||
: sort === "latest"
|
: sort === "recommended"
|
||||||
? t("latest")
|
? t("official")
|
||||||
: sort === "recommended"
|
: sort === "popular"
|
||||||
? t("official")
|
? t("popular")
|
||||||
: t("all");
|
: t("all");
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user