terry-staging #5
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 357 KiB |
|
Before Width: | Height: | Size: 785 B After Width: | Height: | Size: 646 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 445 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 381 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 489 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 414 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 379 KiB |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 504 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 405 KiB |
@@ -172,8 +172,39 @@ export function FigmaBanner() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pagination = hasMultiple ? (
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center gap-1.5 md:gap-2"
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Banner pagination"
|
||||||
|
>
|
||||||
|
{slides.map((slide, index) => {
|
||||||
|
const active = index === activeIndex;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={slide.id}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={active}
|
||||||
|
aria-label={`Go to slide ${index + 1}`}
|
||||||
|
onClick={() => {
|
||||||
|
pauseAutoplay();
|
||||||
|
setActiveIndex(index);
|
||||||
|
goTo(index, "smooth");
|
||||||
|
}}
|
||||||
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
|
active
|
||||||
|
? "w-6 bg-ark-gold"
|
||||||
|
: "w-1.5 bg-[#7C7C7C] hover:bg-white/50"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
ref={scrollerRef}
|
ref={scrollerRef}
|
||||||
onPointerDown={handlePointerDown}
|
onPointerDown={handlePointerDown}
|
||||||
@@ -194,12 +225,12 @@ export function FigmaBanner() {
|
|||||||
aria-roledescription="slide"
|
aria-roledescription="slide"
|
||||||
aria-label={`${index + 1} / ${slides.length}`}
|
aria-label={`${index + 1} / ${slides.length}`}
|
||||||
>
|
>
|
||||||
<picture className="block w-full overflow-hidden rounded-xl bg-black">
|
<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
|
||||||
src={slide.desktop}
|
src={slide.desktop}
|
||||||
alt={slide.alt}
|
alt={slide.alt}
|
||||||
className="pointer-events-none h-[200px] w-full object-cover md:h-auto"
|
className="pointer-events-none h-[219px] w-full object-cover md:h-auto"
|
||||||
width={1280}
|
width={1280}
|
||||||
height={290}
|
height={290}
|
||||||
loading={index === 0 ? "eager" : "lazy"}
|
loading={index === 0 ? "eager" : "lazy"}
|
||||||
@@ -212,34 +243,12 @@ export function FigmaBanner() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{hasMultiple ? (
|
{hasMultiple ? (
|
||||||
<div
|
<>
|
||||||
className="mt-3 flex items-center justify-center gap-2"
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex h-[30px] items-center justify-center bg-gradient-to-b from-[#14131900] to-[#141319] md:hidden">
|
||||||
role="tablist"
|
<div className="pointer-events-auto">{pagination}</div>
|
||||||
aria-label="Banner pagination"
|
</div>
|
||||||
>
|
<div className="mt-3 hidden md:block">{pagination}</div>
|
||||||
{slides.map((slide, index) => {
|
</>
|
||||||
const active = index === activeIndex;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={slide.id}
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
aria-selected={active}
|
|
||||||
aria-label={`Go to slide ${index + 1}`}
|
|
||||||
onClick={() => {
|
|
||||||
pauseAutoplay();
|
|
||||||
setActiveIndex(index);
|
|
||||||
goTo(index, "smooth");
|
|
||||||
}}
|
|
||||||
className={`h-1.5 rounded-full transition-all ${
|
|
||||||
active
|
|
||||||
? "w-6 bg-ark-gold"
|
|
||||||
: "w-1.5 bg-white/30 hover:bg-white/50"
|
|
||||||
}`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function isPlaceholderAsset(path: string | undefined | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CARD_CLASS =
|
const CARD_CLASS =
|
||||||
"group flex w-[232px] shrink-0 flex-col overflow-hidden rounded-xl border border-ark-line bg-ark-panel transition hover:border-ark-gold/55 max-[439px]:w-[232px] min-[440px]:w-[230px] sm:w-[240px] lg:w-[246.4px] min-[1100px]:max-xl:w-[273px] xl:w-[246.4px]";
|
"group flex w-[208px] shrink-0 flex-col overflow-hidden rounded-xl border border-transparent bg-[#1D1E23] transition hover:border-ark-gold/55 md:w-[240px] md:border-ark-line md:bg-ark-panel lg:w-[246.4px] min-[1100px]:max-xl:w-[273px] xl:w-[246.4px]";
|
||||||
|
|
||||||
type RecommendedResource = Resource & {
|
type RecommendedResource = Resource & {
|
||||||
downloadPostId?: string;
|
downloadPostId?: string;
|
||||||
@@ -52,7 +52,7 @@ export function RecommendedCard({
|
|||||||
<article className={CARD_CLASS}>
|
<article className={CARD_CLASS}>
|
||||||
<Link
|
<Link
|
||||||
to={`/resource/${r.id}`}
|
to={`/resource/${r.id}`}
|
||||||
className="relative block aspect-[246.4/138.6] overflow-hidden bg-black"
|
className="relative block h-[108px] overflow-hidden bg-black md:aspect-[246.4/138.6] md:h-auto"
|
||||||
>
|
>
|
||||||
{cover ? (
|
{cover ? (
|
||||||
<img
|
<img
|
||||||
@@ -70,14 +70,14 @@ export function RecommendedCard({
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex min-h-[121px] flex-1 flex-col p-4 pt-3">
|
<div className="flex min-h-[131px] flex-1 flex-col p-4 pt-3 md:min-h-[121px]">
|
||||||
<Link
|
<Link
|
||||||
to={`/resource/${r.id}`}
|
to={`/resource/${r.id}`}
|
||||||
className="text-base font-bold leading-snug text-white line-clamp-2 hover:text-ark-gold2"
|
className="text-[15px] font-semibold leading-[21.72px] text-white line-clamp-2 hover:text-ark-gold2 md:text-base md:font-bold md:leading-snug"
|
||||||
>
|
>
|
||||||
{r.title}
|
{r.title}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="mt-auto flex items-center justify-between gap-2 pt-4 text-xs text-ark-muted">
|
<div className="mt-auto flex items-center justify-between gap-2 pt-4 text-[12px] leading-[17.38px] text-ark-muted">
|
||||||
<div className="min-w-0 truncate">
|
<div className="min-w-0 truncate">
|
||||||
<span className="text-neutral-400">{r.categoryName}</span>
|
<span className="text-neutral-400">{r.categoryName}</span>
|
||||||
<span className="mx-1.5 text-ark-line">·</span>
|
<span className="mx-1.5 text-ark-line">·</span>
|
||||||
@@ -144,7 +144,7 @@ export function ComingSoonRecommendedCard({
|
|||||||
className={`${CARD_CLASS} cursor-default opacity-95`}
|
className={`${CARD_CLASS} cursor-default opacity-95`}
|
||||||
aria-label="即将到来"
|
aria-label="即将到来"
|
||||||
>
|
>
|
||||||
<div className="relative block aspect-[246.4/138.6] overflow-hidden bg-black">
|
<div className="relative block h-[108px] overflow-hidden bg-black md:aspect-[246.4/138.6] md:h-auto">
|
||||||
<img
|
<img
|
||||||
src={cover}
|
src={cover}
|
||||||
alt=""
|
alt=""
|
||||||
@@ -155,8 +155,8 @@ export function ComingSoonRecommendedCard({
|
|||||||
即将到来
|
即将到来
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex min-h-[121px] flex-1 flex-col p-4 pt-3">
|
<div className="flex min-h-[131px] flex-1 flex-col p-4 pt-3 md:min-h-[121px]">
|
||||||
<div className="text-base font-bold leading-snug text-white line-clamp-2">
|
<div className="text-[15px] font-semibold leading-[21.72px] text-white line-clamp-2 md:text-base md:font-bold md:leading-snug">
|
||||||
即将到来
|
即将到来
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-auto pt-4 text-xs text-ark-muted">
|
<div className="mt-auto pt-4 text-xs text-ark-muted">
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export function SectionHeader({
|
|||||||
viewAllLabel,
|
viewAllLabel,
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
title: string;
|
||||||
viewAllTo: string;
|
viewAllTo?: string;
|
||||||
viewAllLabel: string;
|
viewAllLabel?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-[49px] items-center justify-between gap-4 md:h-6">
|
<div className="flex h-[49px] items-center justify-between gap-4 md:h-6">
|
||||||
@@ -21,13 +21,15 @@ export function SectionHeader({
|
|||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
{viewAllTo && viewAllLabel ? (
|
||||||
to={viewAllTo}
|
<Link
|
||||||
className="inline-flex shrink-0 items-center gap-1 text-[13px] font-normal leading-none text-ark-gold hover:text-ark-gold2 md:text-[15px] md:font-medium"
|
to={viewAllTo}
|
||||||
>
|
className="inline-flex shrink-0 items-center gap-1 text-[13px] font-normal leading-none text-ark-gold hover:text-ark-gold2 md:text-[15px] md:font-medium"
|
||||||
{viewAllLabel}
|
>
|
||||||
<ChevronRight className="h-4 w-4" strokeWidth={2.7} />
|
{viewAllLabel}
|
||||||
</Link>
|
<ChevronRight className="h-4 w-4" strokeWidth={2.7} />
|
||||||
|
</Link>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="sticky top-0 z-10 bg-ark-bg/95 backdrop-blur md:rounded-t-xl md:border-b md:border-ark-line">
|
<div className="sticky top-0 z-10 bg-ark-bg/95 backdrop-blur md:rounded-t-xl md:border-b md:border-ark-line">
|
||||||
<div
|
<div
|
||||||
className="relative flex items-end gap-2 overflow-x-auto overflow-y-hidden px-4 pr-8 [-ms-overflow-style:none] [scrollbar-width:none] md:gap-5 md:px-1 md:pr-1 [&::-webkit-scrollbar]:hidden"
|
className="flex items-end gap-2 overflow-x-auto overflow-y-hidden px-4 pr-10 [-ms-overflow-style:none] [scrollbar-width:none] md:gap-5 md:px-1 md:pr-1 [&::-webkit-scrollbar]:hidden"
|
||||||
role="tablist"
|
role="tablist"
|
||||||
>
|
>
|
||||||
{TYPE_FILTERS.map((tp) => {
|
{TYPE_FILTERS.map((tp) => {
|
||||||
@@ -51,11 +51,11 @@ export function FilterChips({ type, onTypeChange }: FilterChipsProps) {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<div
|
|
||||||
className="pointer-events-none absolute right-0 top-0 hidden h-[52px] w-[114px] bg-gradient-to-l from-ark-bg via-ark-bg/85 to-transparent max-md:block"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute right-0 top-0 hidden h-[52px] w-10 bg-gradient-to-l from-ark-bg via-ark-bg/80 to-transparent max-md:block"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const zhDict: Dict = {
|
|||||||
mainNav: "网站导航",
|
mainNav: "网站导航",
|
||||||
home: "首页",
|
home: "首页",
|
||||||
all: "全部资料",
|
all: "全部资料",
|
||||||
categories: "分类浏览",
|
categories: "资料分类",
|
||||||
latest: "最新更新",
|
latest: "最新更新",
|
||||||
official: "官方推荐",
|
official: "官方推荐",
|
||||||
popular: "热门资料",
|
popular: "热门资料",
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ function navIsActive(
|
|||||||
case "categories":
|
case "categories":
|
||||||
return pathname === "/" && hash === "#categories";
|
return pathname === "/" && hash === "#categories";
|
||||||
case "browseLatest":
|
case "browseLatest":
|
||||||
return pathname === "/browse" && sp.get("sort") === "latest";
|
return pathname === "/" && hash === "#latest";
|
||||||
case "browseRecommended":
|
case "browseRecommended":
|
||||||
return pathname === "/browse" && sp.get("sort") === "recommended";
|
return pathname === "/" && hash === "#official";
|
||||||
case "browsePopular":
|
case "browsePopular":
|
||||||
return pathname === "/browse" && sp.get("sort") === "popular";
|
return pathname === "/" && hash === "#popular";
|
||||||
case "about":
|
case "about":
|
||||||
return pathname === "/about";
|
return pathname === "/about";
|
||||||
default:
|
default:
|
||||||
@@ -266,6 +266,7 @@ export function PublicLayout() {
|
|||||||
|
|
||||||
const na = (which: PublicNavWhich) =>
|
const na = (which: PublicNavWhich) =>
|
||||||
navIsActive(pathname, search, hash, which);
|
navIsActive(pathname, search, hash, which);
|
||||||
|
const isHome = pathname === "/";
|
||||||
const footerInContentFlow =
|
const footerInContentFlow =
|
||||||
pathname === "/browse" || pathname.startsWith("/category/");
|
pathname === "/browse" || pathname.startsWith("/category/");
|
||||||
|
|
||||||
@@ -280,13 +281,13 @@ export function PublicLayout() {
|
|||||||
return (
|
return (
|
||||||
<div className="min-h-full flex flex-col">
|
<div className="min-h-full flex flex-col">
|
||||||
<header className="sticky top-0 z-40 bg-[#08070c] backdrop-blur-md md:border-b md:border-ark-line md:bg-ark-nav/98">
|
<header className="sticky top-0 z-40 bg-[#08070c] backdrop-blur-md md:border-b md:border-ark-line md:bg-ark-nav/98">
|
||||||
<div className="flex h-[64px] items-center justify-between bg-[#08070c] px-[20px] py-[12px] md:hidden">
|
<div className="flex h-[64px] items-center justify-between bg-[#08070c] px-4 py-3 md:hidden">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="flex h-[28px] shrink-0 items-center gap-[8px] rounded-sm text-[18px] font-bold tracking-wide text-ark-gold outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070c]"
|
className="flex h-8 shrink-0 items-center gap-2 rounded-sm text-[20px] font-black leading-5 tracking-tight text-ark-gold outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070c]"
|
||||||
aria-label={t("brand")}
|
aria-label={t("brand")}
|
||||||
>
|
>
|
||||||
<ArkLogoMark className="h-[28px] w-[28px] shrink-0" />
|
<ArkLogoMark className="h-8 w-8 shrink-0" />
|
||||||
<span className="truncate text-ark-gold">{t("brand")}</span>
|
<span className="truncate text-ark-gold">{t("brand")}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
@@ -379,13 +380,6 @@ export function PublicLayout() {
|
|||||||
className="header-nav-scroll hidden min-w-0 flex-1 items-center justify-center gap-4 overflow-x-auto overflow-y-hidden py-1 min-[1200px]:flex lg:gap-5"
|
className="header-nav-scroll hidden min-w-0 flex-1 items-center justify-center gap-4 overflow-x-auto overflow-y-hidden py-1 min-[1200px]:flex lg:gap-5"
|
||||||
aria-label={t("mainNav")}
|
aria-label={t("mainNav")}
|
||||||
>
|
>
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className={navClassName(na("home"))}
|
|
||||||
aria-current={na("home") ? "page" : undefined}
|
|
||||||
>
|
|
||||||
{t("home")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
to="/browse"
|
to="/browse"
|
||||||
className={navClassName(na("browseAll"))}
|
className={navClassName(na("browseAll"))}
|
||||||
@@ -401,26 +395,33 @@ export function PublicLayout() {
|
|||||||
{t("categories")}
|
{t("categories")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/browse?sort=latest"
|
to="/#official"
|
||||||
className={navClassName(na("browseLatest"))}
|
|
||||||
aria-current={na("browseLatest") ? "page" : undefined}
|
|
||||||
>
|
|
||||||
{t("latest")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/browse?sort=recommended"
|
|
||||||
className={navClassName(na("browseRecommended"))}
|
className={navClassName(na("browseRecommended"))}
|
||||||
aria-current={na("browseRecommended") ? "page" : undefined}
|
aria-current={na("browseRecommended") ? "page" : undefined}
|
||||||
>
|
>
|
||||||
{t("official")}
|
{t("official")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/browse?sort=popular"
|
to="/#latest"
|
||||||
|
className={navClassName(na("browseLatest"))}
|
||||||
|
aria-current={na("browseLatest") ? "page" : undefined}
|
||||||
|
>
|
||||||
|
{t("latest")}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/#popular"
|
||||||
className={navClassName(na("browsePopular"))}
|
className={navClassName(na("browsePopular"))}
|
||||||
aria-current={na("browsePopular") ? "page" : undefined}
|
aria-current={na("browsePopular") ? "page" : undefined}
|
||||||
>
|
>
|
||||||
{t("popular")}
|
{t("popular")}
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/about"
|
||||||
|
className={navClassName(na("about"))}
|
||||||
|
aria-current={na("about") ? "page" : undefined}
|
||||||
|
>
|
||||||
|
{t("footerAbout")}
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="flex min-w-0 flex-1 items-center justify-end gap-2 min-[1200px]:flex-none">
|
<div className="flex min-w-0 flex-1 items-center justify-end gap-2 min-[1200px]:flex-none">
|
||||||
@@ -470,14 +471,6 @@ export function PublicLayout() {
|
|||||||
ariaLabel={t("langLabel")}
|
ariaLabel={t("langLabel")}
|
||||||
className="mb-1 hidden md:block"
|
className="mb-1 hidden md:block"
|
||||||
/>
|
/>
|
||||||
<Link
|
|
||||||
to="/"
|
|
||||||
className={navClassName(na("home"))}
|
|
||||||
aria-current={na("home") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{t("home")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
to="/browse"
|
to="/browse"
|
||||||
className={navClassName(na("browseAll"))}
|
className={navClassName(na("browseAll"))}
|
||||||
@@ -495,15 +488,7 @@ export function PublicLayout() {
|
|||||||
{t("categories")}
|
{t("categories")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/browse?sort=latest"
|
to="/#official"
|
||||||
className={navClassName(na("browseLatest"))}
|
|
||||||
aria-current={na("browseLatest") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{t("latest")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to="/browse?sort=recommended"
|
|
||||||
className={navClassName(na("browseRecommended"))}
|
className={navClassName(na("browseRecommended"))}
|
||||||
aria-current={na("browseRecommended") ? "page" : undefined}
|
aria-current={na("browseRecommended") ? "page" : undefined}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
@@ -511,7 +496,15 @@ export function PublicLayout() {
|
|||||||
{t("official")}
|
{t("official")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/browse?sort=popular"
|
to="/#latest"
|
||||||
|
className={navClassName(na("browseLatest"))}
|
||||||
|
aria-current={na("browseLatest") ? "page" : undefined}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
{t("latest")}
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to="/#popular"
|
||||||
className={navClassName(na("browsePopular"))}
|
className={navClassName(na("browsePopular"))}
|
||||||
aria-current={na("browsePopular") ? "page" : undefined}
|
aria-current={na("browsePopular") ? "page" : undefined}
|
||||||
onClick={() => setOpen(false)}
|
onClick={() => setOpen(false)}
|
||||||
@@ -532,19 +525,17 @@ export function PublicLayout() {
|
|||||||
|
|
||||||
<main
|
<main
|
||||||
className={`mx-auto w-full max-w-[1280px] ${
|
className={`mx-auto w-full max-w-[1280px] ${
|
||||||
footerInContentFlow
|
isHome
|
||||||
? "px-0 pb-0 pt-0 md:px-9 md:pt-10 xl:px-0"
|
? "flex-1 px-0 pb-6 pt-0 md:px-9 md:pb-10 md:pt-10 xl:px-0"
|
||||||
: "flex-1 px-4 pb-6 pt-6 min-[440px]:px-5 sm:px-6 md:px-9 md:pb-10 md:pt-10 xl:px-0"
|
: footerInContentFlow
|
||||||
|
? "px-0 pb-0 pt-0 md:px-9 md:pt-10 xl:px-0"
|
||||||
|
: "flex-1 px-4 pb-6 pt-6 min-[440px]:px-5 sm:px-6 md:px-9 md:pb-10 md:pt-10 xl:px-0"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer
|
<footer className="mt-auto bg-transparent md:border-t md:border-ark-line md:bg-ark-nav/90">
|
||||||
className={`bg-transparent md:border-t md:border-ark-line md:bg-ark-nav/90 ${
|
|
||||||
footerInContentFlow ? "mt-3" : "mt-auto"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div className="mx-auto flex h-[52px] max-w-[358px] items-center justify-center px-4 py-4 text-[13px] leading-5 md:h-auto md:max-w-[1280px] md:justify-start md:px-9 md:py-6 md:text-sm xl:px-0">
|
<div className="mx-auto flex h-[52px] max-w-[358px] items-center justify-center px-4 py-4 text-[13px] leading-5 md:h-auto md:max-w-[1280px] md:justify-start md:px-9 md:py-6 md:text-sm xl:px-0">
|
||||||
<Link
|
<Link
|
||||||
to="/about"
|
to="/about"
|
||||||
@@ -558,7 +549,7 @@ export function PublicLayout() {
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<nav className="sticky inset-x-0 bottom-0 z-40 bg-[#0C0D0F]/90 backdrop-blur md:hidden">
|
<nav className="sticky inset-x-0 bottom-0 z-40 bg-[#0C0D0F]/90 backdrop-blur md:hidden">
|
||||||
<div className="grid h-[78px] grid-cols-4 gap-3 px-5 py-4 text-center text-[13px] leading-[18px]">
|
<div className="grid h-[78px] grid-cols-4 gap-3 px-5 py-4 text-center text-[11px] leading-[17.6px]">
|
||||||
<BottomNavIcon
|
<BottomNavIcon
|
||||||
to="/"
|
to="/"
|
||||||
label={t("home")}
|
label={t("home")}
|
||||||
@@ -580,13 +571,10 @@ export function PublicLayout() {
|
|||||||
active={pathname === "/favorites"}
|
active={pathname === "/favorites"}
|
||||||
/>
|
/>
|
||||||
<BottomNavIcon
|
<BottomNavIcon
|
||||||
to="/browse?sort=latest"
|
to="/#latest"
|
||||||
label={t("latest")}
|
label={t("latest")}
|
||||||
icon="update"
|
icon="update"
|
||||||
active={
|
active={pathname === "/" && hash === "#latest"}
|
||||||
pathname === "/browse" &&
|
|
||||||
new URLSearchParams(search).get("sort") === "latest"
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -10,11 +10,7 @@ export function Browse() {
|
|||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<div className="mx-auto max-w-full px-4 md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
|
<div className="mx-auto max-w-full px-4 md:max-w-[820px] lg:max-w-[1080px] xl:max-w-[1180px]">
|
||||||
<SectionHeader
|
<SectionHeader title={q ? `${t("search")}: ${q}` : t("all")} />
|
||||||
title={q ? `${t("search")}: ${q}` : t("all")}
|
|
||||||
viewAllTo="/browse"
|
|
||||||
viewAllLabel={t("viewAll")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<MessageStream scope={{ kind: "all" }} />
|
<MessageStream scope={{ kind: "all" }} />
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from "../../components/LatestUpdateRow";
|
} from "../../components/LatestUpdateRow";
|
||||||
import { RecommendedCard } from "../../components/RecommendedCard";
|
import { RecommendedCard } from "../../components/RecommendedCard";
|
||||||
import { SectionHeader } from "../../components/SectionHeader";
|
import { SectionHeader } from "../../components/SectionHeader";
|
||||||
|
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 { categoryCardLines } from "../../utils/categoryDisplay";
|
import { categoryCardLines } from "../../utils/categoryDisplay";
|
||||||
@@ -24,6 +25,7 @@ export function Home() {
|
|||||||
const [cats, setCats] = useState<Category[]>([]);
|
const [cats, setCats] = useState<Category[]>([]);
|
||||||
const [rec, setRec] = useState<PostBackedResource[]>([]);
|
const [rec, setRec] = useState<PostBackedResource[]>([]);
|
||||||
const [latest, setLatest] = useState<PostBackedResource[]>([]);
|
const [latest, setLatest] = useState<PostBackedResource[]>([]);
|
||||||
|
const [latestPosts, setLatestPosts] = useState<Post[]>([]);
|
||||||
const [err, setErr] = useState<string | null>(null);
|
const [err, setErr] = useState<string | null>(null);
|
||||||
const recRowRef = useRef<HTMLDivElement>(null);
|
const recRowRef = useRef<HTMLDivElement>(null);
|
||||||
const categoryRowRef = useRef<HTMLDivElement>(null);
|
const categoryRowRef = useRef<HTMLDivElement>(null);
|
||||||
@@ -48,8 +50,10 @@ export function Home() {
|
|||||||
postToResource(post, lang, itemsOrEmpty(c)),
|
postToResource(post, lang, itemsOrEmpty(c)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const latestItems = itemsOrEmpty(l.items);
|
||||||
|
setLatestPosts(latestItems);
|
||||||
setLatest(
|
setLatest(
|
||||||
itemsOrEmpty(l.items).map((post) =>
|
latestItems.map((post) =>
|
||||||
postToResource(post, lang, itemsOrEmpty(c)),
|
postToResource(post, lang, itemsOrEmpty(c)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -64,6 +68,10 @@ export function Home() {
|
|||||||
for (let index = 0; index < cats.length; index += 9) {
|
for (let index = 0; index < cats.length; index += 9) {
|
||||||
categoryPages.push(cats.slice(index, index + 9));
|
categoryPages.push(cats.slice(index, index + 9));
|
||||||
}
|
}
|
||||||
|
const activeCategoryCount = categoryPages[activeCategoryPage]?.length ?? 0;
|
||||||
|
const activeCategoryRows = Math.ceil(activeCategoryCount / 3);
|
||||||
|
const mobileCategoryHeight =
|
||||||
|
activeCategoryRows * 88 + Math.max(0, activeCategoryRows - 1) * 8;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const row = categoryRowRef.current;
|
const row = categoryRowRef.current;
|
||||||
@@ -119,6 +127,11 @@ export function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const latestPlaceholderCount = Math.max(0, 5 - latest.length);
|
const latestPlaceholderCount = Math.max(0, 5 - latest.length);
|
||||||
|
const recommendedDotCount = Math.max(1, Math.min(4, rec.length || 4));
|
||||||
|
const activeRecommendedDot = Math.min(
|
||||||
|
recommendedDotCount - 1,
|
||||||
|
Math.round(recScroll.progress * (recommendedDotCount - 1)),
|
||||||
|
);
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
return (
|
return (
|
||||||
@@ -129,28 +142,31 @@ export function Home() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-[30px] pb-10 md:space-y-10 md:pb-16 xl:space-y-[34px]">
|
<div className="pb-4 md:space-y-10 md:pb-16 xl:space-y-[34px]">
|
||||||
<section className="-mt-6 md:mt-0">
|
<section className="md:mt-0">
|
||||||
<FigmaBanner />
|
<FigmaBanner />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="categories" className="scroll-mt-24">
|
<section id="categories" className="scroll-mt-24">
|
||||||
<SectionHeader
|
<div className="px-4 md:px-0">
|
||||||
title={t("categorySection")}
|
<SectionHeader
|
||||||
viewAllTo="/browse"
|
title={t("categorySection")}
|
||||||
viewAllLabel={t("viewAll")}
|
viewAllTo="/browse"
|
||||||
/>
|
viewAllLabel={t("viewAll")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="md:hidden">
|
<div className="md:hidden">
|
||||||
<div
|
<div
|
||||||
ref={categoryRowRef}
|
ref={categoryRowRef}
|
||||||
className="flex snap-x snap-mandatory overflow-x-auto overflow-y-hidden scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
className="flex snap-x snap-mandatory items-start overflow-x-auto overflow-y-hidden scroll-smooth transition-[height] duration-300 ease-out motion-reduce:transition-none [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
|
style={{ height: `${mobileCategoryHeight}px` }}
|
||||||
aria-label={t("categorySection")}
|
aria-label={t("categorySection")}
|
||||||
>
|
>
|
||||||
{categoryPages.map((page, pageIndex) => (
|
{categoryPages.map((page, pageIndex) => (
|
||||||
<div
|
<div
|
||||||
key={`category-page-${pageIndex}`}
|
key={`category-page-${pageIndex}`}
|
||||||
className="grid w-full shrink-0 snap-start grid-cols-3 gap-2"
|
className="grid w-full shrink-0 snap-start grid-cols-3 gap-2 px-4"
|
||||||
>
|
>
|
||||||
{page.map((c) => (
|
{page.map((c) => (
|
||||||
<Link
|
<Link
|
||||||
@@ -233,16 +249,18 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section id="official" className="scroll-mt-24">
|
||||||
<SectionHeader
|
<div className="px-4 md:px-0">
|
||||||
title={t("officialSection")}
|
<SectionHeader
|
||||||
viewAllTo="/browse?sort=recommended"
|
title={t("officialSection")}
|
||||||
viewAllLabel={t("viewAll")}
|
viewAllTo="/browse?sort=recommended"
|
||||||
/>
|
viewAllLabel={t("viewAll")}
|
||||||
<div className="relative mt-7">
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
<div
|
<div
|
||||||
ref={recRowRef}
|
ref={recRowRef}
|
||||||
className="flex gap-3 overflow-x-auto pb-5 pr-0 scroll-smooth snap-x snap-mandatory [-ms-overflow-style:none] [scrollbar-width:none] md:gap-4 [&::-webkit-scrollbar]:hidden"
|
className="flex snap-x snap-mandatory gap-3 overflow-x-auto overflow-y-hidden px-4 pb-0 pr-4 scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] md:mt-7 md:gap-4 md:px-0 md:pb-5 [&::-webkit-scrollbar]:hidden"
|
||||||
>
|
>
|
||||||
{rec.map((r, index) => (
|
{rec.map((r, index) => (
|
||||||
<div key={r.id} className="snap-start">
|
<div key={r.id} className="snap-start">
|
||||||
@@ -250,14 +268,38 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="h-1 overflow-hidden rounded-full bg-black/80 md:hidden">
|
<div
|
||||||
<div
|
className="flex h-[30px] items-center justify-center gap-1.5 md:hidden"
|
||||||
className="h-full rounded-full bg-ark-gold transition-transform duration-300 ease-out"
|
aria-label="Recommended pagination"
|
||||||
style={{
|
>
|
||||||
width: `${Math.round(recScroll.ratio * 100)}%`,
|
{Array.from({ length: recommendedDotCount }).map((_, index) => (
|
||||||
transform: `translateX(${recScroll.progress * (100 / recScroll.ratio - 100)}%)`,
|
<button
|
||||||
}}
|
key={`recommended-dot-${index}`}
|
||||||
/>
|
type="button"
|
||||||
|
aria-label={`Go to recommendation page ${index + 1}`}
|
||||||
|
aria-current={activeRecommendedDot === index}
|
||||||
|
onClick={() => {
|
||||||
|
const row = recRowRef.current;
|
||||||
|
if (!row) return;
|
||||||
|
const maxScroll = Math.max(
|
||||||
|
0,
|
||||||
|
row.scrollWidth - row.clientWidth,
|
||||||
|
);
|
||||||
|
row.scrollTo({
|
||||||
|
left:
|
||||||
|
recommendedDotCount === 1
|
||||||
|
? 0
|
||||||
|
: (maxScroll * index) / (recommendedDotCount - 1),
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className={`h-1.5 rounded-full transition-all ${
|
||||||
|
activeRecommendedDot === index
|
||||||
|
? "w-6 bg-ark-gold"
|
||||||
|
: "w-1.5 bg-[#7C7C7C]"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
{canScrollRec ? (
|
{canScrollRec ? (
|
||||||
<button
|
<button
|
||||||
@@ -272,13 +314,20 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section id="latest" className="scroll-mt-24">
|
||||||
<SectionHeader
|
<div className="px-4 md:px-0">
|
||||||
title={t("latestSection")}
|
<SectionHeader
|
||||||
viewAllTo="/browse?sort=latest"
|
title={t("latestSection")}
|
||||||
viewAllLabel={t("viewAll")}
|
viewAllTo="/browse?sort=latest"
|
||||||
/>
|
viewAllLabel={t("viewAll")}
|
||||||
<div className="mt-7 grid gap-3 min-[576px]:grid-cols-2 md:gap-4 lg:grid-cols-3 xl:grid-cols-5">
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 md:hidden">
|
||||||
|
{latestPosts.slice(0, 4).map((post) => (
|
||||||
|
<MessageBubble key={post.id} post={post} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="mt-7 hidden gap-3 min-[576px]:grid-cols-2 md:grid md:gap-4 lg:grid-cols-3 xl:grid-cols-5">
|
||||||
{latest.map((r) => (
|
{latest.map((r) => (
|
||||||
<LatestUpdateRow key={r.id} r={r} iconKey={iconKeyForResource(r)} />
|
<LatestUpdateRow key={r.id} r={r} iconKey={iconKeyForResource(r)} />
|
||||||
))}
|
))}
|
||||||
@@ -290,6 +339,8 @@ export function Home() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<span id="popular" className="block scroll-mt-24" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||