terry-staging #5
Binary file not shown.
|
After Width: | Height: | Size: 253 KiB |
@@ -9,11 +9,7 @@ import {
|
|||||||
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
|
const FIGMA_ASSET_BASE = "/assets/ark-library/figma";
|
||||||
|
|
||||||
export const officialRecommendationCoverFallbacks = [
|
export const officialRecommendationCoverFallbacks = [
|
||||||
`${FIGMA_ASSET_BASE}/official-recommendation-1.png`,
|
`${FIGMA_ASSET_BASE}/official-recommendation-cover.png`,
|
||||||
`${FIGMA_ASSET_BASE}/official-recommendation-2.png`,
|
|
||||||
`${FIGMA_ASSET_BASE}/official-recommendation-3.png`,
|
|
||||||
`${FIGMA_ASSET_BASE}/official-recommendation-4.png`,
|
|
||||||
`${FIGMA_ASSET_BASE}/official-recommendation-5.png`,
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type BannerSlide = {
|
type BannerSlide = {
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import type { Resource } from "../api";
|
|||||||
import { assetUrl } from "../api";
|
import { assetUrl } from "../api";
|
||||||
import { useI18n } from "../i18n";
|
import { useI18n } from "../i18n";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { cleanCategoryDisplayName } from "../utils/categoryDisplay";
|
|
||||||
import { formatDateYmd } from "../utils/format";
|
import { formatDateYmd } from "../utils/format";
|
||||||
|
import { DownloadCloudIcon } from "./icons/DownloadCloudIcon";
|
||||||
import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
|
import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
|
||||||
import {
|
import {
|
||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
@@ -17,7 +17,7 @@ function isPlaceholderAsset(path: string | undefined | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CARD_CLASS =
|
const CARD_CLASS =
|
||||||
"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]";
|
"group flex w-[208px] shrink-0 flex-col overflow-hidden rounded-xl border bg-[#1D1E23] transition hover:border-ark-gold/55 md:w-[240px] lg:w-[246.4px] min-[1100px]:max-xl:w-[273px] xl:w-[246.4px]";
|
||||||
|
|
||||||
type RecommendedResource = Resource & {
|
type RecommendedResource = Resource & {
|
||||||
downloadPostId?: string;
|
downloadPostId?: string;
|
||||||
@@ -48,7 +48,7 @@ export function RecommendedCard({
|
|||||||
return assetUrl(original);
|
return assetUrl(original);
|
||||||
}, [figmaCover, r.coverImage, r.previewUrl, useFigmaDesign]);
|
}, [figmaCover, r.coverImage, r.previewUrl, useFigmaDesign]);
|
||||||
const displayTitle = r.title;
|
const displayTitle = r.title;
|
||||||
const displayCategoryName = cleanCategoryDisplayName(r.categoryName);
|
const displayCategoryName = r.categoryName;
|
||||||
const dateStr = formatDateYmd(r.updatedAt);
|
const dateStr = formatDateYmd(r.updatedAt);
|
||||||
const dateTime = r.updatedAt;
|
const dateTime = r.updatedAt;
|
||||||
|
|
||||||
@@ -57,56 +57,7 @@ export function RecommendedCard({
|
|||||||
? assetUrl(r.fileUrl || r.previewUrl)
|
? assetUrl(r.fileUrl || r.previewUrl)
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
return (
|
const handleDownload = async () => {
|
||||||
<article className={CARD_CLASS}>
|
|
||||||
<Link
|
|
||||||
to={`/resource/${r.id}`}
|
|
||||||
className="relative block h-[108px] overflow-hidden bg-black md:aspect-[246.4/138.6] md:h-auto"
|
|
||||||
>
|
|
||||||
{cover ? (
|
|
||||||
<img
|
|
||||||
src={cover}
|
|
||||||
alt=""
|
|
||||||
className={`h-full w-full object-cover transition duration-300 ${
|
|
||||||
useFigmaDesign
|
|
||||||
? "origin-top scale-[1.08] group-hover:scale-[1.1]"
|
|
||||||
: "group-hover:scale-[1.02]"
|
|
||||||
}`}
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full bg-gradient-to-br from-neutral-900 to-neutral-950" />
|
|
||||||
)}
|
|
||||||
{r.badgeLabel ? (
|
|
||||||
<span className="absolute left-3 top-3 rounded-md bg-ark-gold px-2.5 py-1 text-xs font-semibold text-black">
|
|
||||||
{r.badgeLabel}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</Link>
|
|
||||||
<div className="flex min-h-[131px] flex-1 flex-col p-4 pt-3 md:min-h-[121px]">
|
|
||||||
<Link
|
|
||||||
to={`/resource/${r.id}`}
|
|
||||||
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"
|
|
||||||
>
|
|
||||||
{displayTitle}
|
|
||||||
</Link>
|
|
||||||
<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">
|
|
||||||
<span className="text-neutral-400">{displayCategoryName}</span>
|
|
||||||
<span className="mx-1.5 text-ark-line">·</span>
|
|
||||||
<time dateTime={dateTime}>{dateStr}</time>
|
|
||||||
</div>
|
|
||||||
{dl ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="shrink-0 rounded-lg p-1 text-ark-gold outline-none hover:bg-ark-gold/10 focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
|
|
||||||
title={isDownloading ? t("downloading") : t("download")}
|
|
||||||
aria-label={isDownloading ? t("downloading") : t("download")}
|
|
||||||
aria-busy={isDownloading}
|
|
||||||
disabled={isDownloading}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
if (isDownloading) return;
|
if (isDownloading) return;
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
try {
|
try {
|
||||||
@@ -124,13 +75,111 @@ export function RecommendedCard({
|
|||||||
} finally {
|
} finally {
|
||||||
setIsDownloading(false);
|
setIsDownloading(false);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={`${CARD_CLASS} ${
|
||||||
|
useFigmaDesign
|
||||||
|
? "border-[#27292E]"
|
||||||
|
: "border-transparent md:border-ark-line md:bg-ark-panel"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
to={`/resource/${r.id}`}
|
||||||
|
className="relative block h-[108px] overflow-hidden bg-[#111116] md:aspect-[246.4/138.6] md:h-auto"
|
||||||
|
>
|
||||||
|
{cover ? (
|
||||||
|
<img
|
||||||
|
src={cover}
|
||||||
|
alt=""
|
||||||
|
className={`h-full w-full object-cover transition duration-300 ${
|
||||||
|
useFigmaDesign
|
||||||
|
? "group-hover:scale-[1.02]"
|
||||||
|
: "group-hover:scale-[1.02]"
|
||||||
|
}`}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full bg-gradient-to-br from-neutral-900 to-neutral-950" />
|
||||||
|
)}
|
||||||
|
{!useFigmaDesign && r.badgeLabel ? (
|
||||||
|
<span className="absolute left-3 top-3 rounded-md bg-ark-gold px-2.5 py-1 text-xs font-semibold text-black">
|
||||||
|
{r.badgeLabel}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</Link>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
useFigmaDesign
|
||||||
|
? "flex h-[131px] flex-col px-4 py-3"
|
||||||
|
: "flex min-h-[131px] flex-1 flex-col p-4 pt-3 md:min-h-[121px]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<Link
|
||||||
|
to={`/resource/${r.id}`}
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{displayTitle}
|
||||||
|
</Link>
|
||||||
|
{useFigmaDesign ? (
|
||||||
|
<div className="truncate text-[12px] font-normal leading-[17.376px] text-[#A8A9AE]">
|
||||||
|
{displayCategoryName}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
useFigmaDesign
|
||||||
|
? "mt-auto flex h-10 items-end justify-between gap-2"
|
||||||
|
: "mt-auto flex items-center justify-between gap-2 pt-4 text-[12px] leading-[17.38px] text-ark-muted"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
useFigmaDesign
|
||||||
|
? "min-w-0 truncate pb-[1px] text-[12px] font-normal leading-[17.376px] text-[#A8A9AE]"
|
||||||
|
: "min-w-0 truncate"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{useFigmaDesign ? null : (
|
||||||
|
<>
|
||||||
|
<span className="text-neutral-400">{displayCategoryName}</span>
|
||||||
|
<span className="mx-1.5 text-ark-line">·</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<time dateTime={dateTime}>{dateStr}</time>
|
||||||
|
</div>
|
||||||
|
{dl ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
useFigmaDesign
|
||||||
|
? "flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[#191921] text-ark-gold outline-none transition hover:bg-[#22232D] focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
|
||||||
|
: "shrink-0 rounded-lg p-1 text-ark-gold outline-none hover:bg-ark-gold/10 focus-visible:ring-2 focus-visible:ring-ark-gold/80 disabled:cursor-wait"
|
||||||
|
}
|
||||||
|
title={isDownloading ? t("downloading") : t("download")}
|
||||||
|
aria-label={isDownloading ? t("downloading") : t("download")}
|
||||||
|
aria-busy={isDownloading}
|
||||||
|
disabled={isDownloading}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleDownload();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isDownloading ? (
|
{isDownloading ? (
|
||||||
<LoaderCircle
|
<LoaderCircle
|
||||||
className="h-5 w-5 animate-spin"
|
className={
|
||||||
|
useFigmaDesign
|
||||||
|
? "h-5 w-5 animate-spin"
|
||||||
|
: "h-5 w-5 animate-spin"
|
||||||
|
}
|
||||||
strokeWidth={2.2}
|
strokeWidth={2.2}
|
||||||
/>
|
/>
|
||||||
|
) : useFigmaDesign ? (
|
||||||
|
<DownloadCloudIcon className="h-6 w-6" />
|
||||||
) : (
|
) : (
|
||||||
<Download className="h-5 w-5" strokeWidth={2.2} />
|
<Download className="h-5 w-5" strokeWidth={2.2} />
|
||||||
)}
|
)}
|
||||||
@@ -154,7 +203,7 @@ export function ComingSoonRecommendedCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
className={`${CARD_CLASS} cursor-default opacity-95`}
|
className={`${CARD_CLASS} cursor-default border-transparent opacity-95 md:border-ark-line md:bg-ark-panel`}
|
||||||
aria-label="即将到来"
|
aria-label="即将到来"
|
||||||
>
|
>
|
||||||
<div className="relative block h-[108px] overflow-hidden bg-black md:aspect-[246.4/138.6] md:h-auto">
|
<div className="relative block h-[108px] overflow-hidden bg-black md:aspect-[246.4/138.6] md:h-auto">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ChevronRight } from "lucide-react";
|
import { ChevronRight } from "lucide-react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { getJSON, itemsOrEmpty, type Category } from "../../api";
|
import { getJSON, itemsOrEmpty, type Category } from "../../api";
|
||||||
import { CategoryIcon } from "../../components/CategoryIcon";
|
import { CategoryIcon } from "../../components/CategoryIcon";
|
||||||
@@ -44,10 +44,13 @@ function figmaCategoryRank(category: Category): number {
|
|||||||
|
|
||||||
export function Home() {
|
export function Home() {
|
||||||
const { t, lang } = useI18n();
|
const { t, lang } = useI18n();
|
||||||
|
const { hash } = useLocation();
|
||||||
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 [latestPosts, setLatestPosts] = useState<Post[]>([]);
|
||||||
|
const [popular, setPopular] = useState<PostBackedResource[]>([]);
|
||||||
|
const [popularPosts, setPopularPosts] = 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);
|
||||||
@@ -64,8 +67,11 @@ export function Home() {
|
|||||||
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=8`),
|
getJSON<{ items: Post[] }>(`/api/posts/latest${postQ}&limit=8`),
|
||||||
|
getJSON<{ items: Post[] }>(
|
||||||
|
`/api/posts${postQ}&sort=popular&limit=8`,
|
||||||
|
).catch((): { items: Post[] } => ({ items: [] })),
|
||||||
])
|
])
|
||||||
.then(([c, r, l]) => {
|
.then(([c, r, l, p]) => {
|
||||||
setCats(itemsOrEmpty(c));
|
setCats(itemsOrEmpty(c));
|
||||||
setRec(
|
setRec(
|
||||||
itemsOrEmpty(r.items).map((post) =>
|
itemsOrEmpty(r.items).map((post) =>
|
||||||
@@ -79,6 +85,13 @@ export function Home() {
|
|||||||
postToResource(post, lang, itemsOrEmpty(c)),
|
postToResource(post, lang, itemsOrEmpty(c)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
const popularItems = itemsOrEmpty<Post>(p.items);
|
||||||
|
setPopularPosts(popularItems);
|
||||||
|
setPopular(
|
||||||
|
popularItems.map((post) =>
|
||||||
|
postToResource(post, lang, itemsOrEmpty(c)),
|
||||||
|
),
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch((e) => setErr(String(e)));
|
.catch((e) => setErr(String(e)));
|
||||||
}, [lang]);
|
}, [lang]);
|
||||||
@@ -152,7 +165,18 @@ export function Home() {
|
|||||||
recRowRef.current?.scrollBy({ left: dir * 280, behavior: "smooth" });
|
recRowRef.current?.scrollBy({ left: dir * 280, behavior: "smooth" });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hash) return;
|
||||||
|
const id = hash.slice(1);
|
||||||
|
if (!id) return;
|
||||||
|
const frame = window.requestAnimationFrame(() => {
|
||||||
|
document.getElementById(id)?.scrollIntoView({ block: "start" });
|
||||||
|
});
|
||||||
|
return () => window.cancelAnimationFrame(frame);
|
||||||
|
}, [hash, cats.length, rec.length, latest.length, popular.length]);
|
||||||
|
|
||||||
const latestPlaceholderCount = Math.max(0, 5 - latest.length);
|
const latestPlaceholderCount = Math.max(0, 5 - latest.length);
|
||||||
|
const popularPlaceholderCount = Math.max(0, 5 - popular.length);
|
||||||
const recommendedDotCount = Math.max(1, Math.min(4, rec.length || 4));
|
const recommendedDotCount = Math.max(1, Math.min(4, rec.length || 4));
|
||||||
const activeRecommendedDot = Math.min(
|
const activeRecommendedDot = Math.min(
|
||||||
recommendedDotCount - 1,
|
recommendedDotCount - 1,
|
||||||
@@ -356,7 +380,31 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<span id="popular" className="block scroll-mt-24" aria-hidden="true" />
|
<section id="popular" className="scroll-mt-24">
|
||||||
|
<div className="px-4 md:px-0">
|
||||||
|
<SectionHeader
|
||||||
|
title={t("popularSection")}
|
||||||
|
viewAllTo="/browse?sort=popular"
|
||||||
|
viewAllLabel={t("viewAll")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-3 md:hidden">
|
||||||
|
{popularPosts.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">
|
||||||
|
{popular.map((r) => (
|
||||||
|
<LatestUpdateRow key={r.id} r={r} iconKey={iconKeyForResource(r)} />
|
||||||
|
))}
|
||||||
|
{Array.from({ length: popularPlaceholderCount }).map((_, index) => (
|
||||||
|
<ComingSoonLatestUpdateRow
|
||||||
|
key={`popular-coming-soon-${index}`}
|
||||||
|
index={popular.length + index}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user