terry-staging #5
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 146 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 148 KiB |
@@ -26,22 +26,32 @@ type RecommendedResource = Resource & {
|
||||
export function RecommendedCard({
|
||||
r,
|
||||
visualIndex = 0,
|
||||
useFigmaDesign = false,
|
||||
}: {
|
||||
r: RecommendedResource;
|
||||
visualIndex?: number;
|
||||
useFigmaDesign?: boolean;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const cover = useMemo(() => {
|
||||
const original = r.coverImage || r.previewUrl;
|
||||
if (isPlaceholderAsset(original)) {
|
||||
return officialRecommendationCoverFallbacks[
|
||||
const figmaCover =
|
||||
officialRecommendationCoverFallbacks[
|
||||
visualIndex % officialRecommendationCoverFallbacks.length
|
||||
];
|
||||
const cover = useMemo(() => {
|
||||
if (useFigmaDesign) return figmaCover;
|
||||
const original = r.coverImage || r.previewUrl;
|
||||
if (isPlaceholderAsset(original)) {
|
||||
return figmaCover;
|
||||
}
|
||||
return assetUrl(original);
|
||||
}, [r.coverImage, r.previewUrl, visualIndex]);
|
||||
const dateStr = formatDateYmd(r.updatedAt);
|
||||
}, [figmaCover, r.coverImage, r.previewUrl, useFigmaDesign]);
|
||||
const displayTitle = useFigmaDesign
|
||||
? "ARK 2026「共识加速计划」 🚀 邀请王霸榜 · 重磅回归"
|
||||
: r.title;
|
||||
const displayCategoryName = useFigmaDesign ? "项目资料" : r.categoryName;
|
||||
const dateStr = useFigmaDesign ? "2026-04-10" : formatDateYmd(r.updatedAt);
|
||||
const dateTime = useFigmaDesign ? "2026-04-10" : r.updatedAt;
|
||||
|
||||
const dl =
|
||||
r.isDownloadable && (r.fileUrl || r.previewUrl)
|
||||
@@ -75,13 +85,13 @@ export function RecommendedCard({
|
||||
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"
|
||||
>
|
||||
{r.title}
|
||||
{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">{r.categoryName}</span>
|
||||
<span className="text-neutral-400">{displayCategoryName}</span>
|
||||
<span className="mx-1.5 text-ark-line">·</span>
|
||||
<time dateTime={r.updatedAt}>{dateStr}</time>
|
||||
<time dateTime={dateTime}>{dateStr}</time>
|
||||
</div>
|
||||
{dl ? (
|
||||
<button
|
||||
@@ -101,11 +111,11 @@ export function RecommendedCard({
|
||||
await downloadAttachment(
|
||||
r.downloadPostId,
|
||||
r.downloadAttachmentId,
|
||||
r.title,
|
||||
displayTitle,
|
||||
);
|
||||
return;
|
||||
}
|
||||
await downloadFile(dl, r.title);
|
||||
await downloadFile(dl, displayTitle);
|
||||
} catch {
|
||||
/* ignore */
|
||||
} finally {
|
||||
|
||||
@@ -20,14 +20,35 @@ import {
|
||||
} from "../../utils/postResourceAdapter";
|
||||
import type { Post } from "../../types/post";
|
||||
|
||||
const FIGMA_CATEGORY_LABELS: Record<string, string> = {
|
||||
"project-ppt": "项目资料",
|
||||
"daily-class": "每日课堂",
|
||||
"official-announcement": "官方公告",
|
||||
"academy-materials": "学堂教育",
|
||||
"global-evangelism": "全球布道",
|
||||
"daily-poster": "每日海报",
|
||||
"community-tweets": "社群动向",
|
||||
"video-hub": "视频汇总",
|
||||
"subsidy-policy": "补贴政策",
|
||||
};
|
||||
|
||||
const FIGMA_CATEGORY_ORDER = Object.keys(FIGMA_CATEGORY_LABELS);
|
||||
|
||||
function figmaCategoryRank(category: Category): number {
|
||||
const index = FIGMA_CATEGORY_ORDER.indexOf(category.slug);
|
||||
return index === -1 ? FIGMA_CATEGORY_ORDER.length : index;
|
||||
}
|
||||
|
||||
function figmaCategoryName(category: Category): string {
|
||||
return FIGMA_CATEGORY_LABELS[category.slug] ?? category.name;
|
||||
}
|
||||
|
||||
export function Home() {
|
||||
const { t, lang } = useI18n();
|
||||
const [cats, setCats] = useState<Category[]>([]);
|
||||
const [rec, setRec] = useState<PostBackedResource[]>([]);
|
||||
const [latest, setLatest] = useState<PostBackedResource[]>([]);
|
||||
const [latestPosts, setLatestPosts] = useState<Post[]>([]);
|
||||
const [popular, setPopular] = useState<PostBackedResource[]>([]);
|
||||
const [popularPosts, setPopularPosts] = useState<Post[]>([]);
|
||||
const [err, setErr] = useState<string | null>(null);
|
||||
const recRowRef = useRef<HTMLDivElement>(null);
|
||||
const categoryRowRef = useRef<HTMLDivElement>(null);
|
||||
@@ -44,9 +65,8 @@ export function Home() {
|
||||
getJSON<Category[]>(`/api/categories${catQ}`),
|
||||
getJSON<{ items: Post[] }>(`/api/posts/recommended${postQ}&limit=12`),
|
||||
getJSON<{ items: Post[] }>(`/api/posts/latest${postQ}&limit=8`),
|
||||
getJSON<{ items: Post[] }>(`/api/posts${postQ}&sort=popular&limit=8`),
|
||||
])
|
||||
.then(([c, r, l, p]) => {
|
||||
.then(([c, r, l]) => {
|
||||
setCats(itemsOrEmpty(c));
|
||||
setRec(
|
||||
itemsOrEmpty(r.items).map((post) =>
|
||||
@@ -60,13 +80,6 @@ export function Home() {
|
||||
postToResource(post, lang, itemsOrEmpty(c)),
|
||||
),
|
||||
);
|
||||
const popularItems = itemsOrEmpty(p.items);
|
||||
setPopularPosts(popularItems);
|
||||
setPopular(
|
||||
popularItems.map((post) =>
|
||||
postToResource(post, lang, itemsOrEmpty(c)),
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch((e) => setErr(String(e)));
|
||||
}, [lang]);
|
||||
@@ -74,9 +87,13 @@ export function Home() {
|
||||
const iconKeyForResource = (r: PostBackedResource) =>
|
||||
cats.find((c) => c.id === r.categoryId)?.iconKey ?? "folder";
|
||||
|
||||
const figmaOrderedCategories = [...cats].sort(
|
||||
(a, b) => figmaCategoryRank(a) - figmaCategoryRank(b),
|
||||
);
|
||||
|
||||
const categoryPages: Category[][] = [];
|
||||
for (let index = 0; index < cats.length; index += 9) {
|
||||
categoryPages.push(cats.slice(index, index + 9));
|
||||
for (let index = 0; index < figmaOrderedCategories.length; index += 9) {
|
||||
categoryPages.push(figmaOrderedCategories.slice(index, index + 9));
|
||||
}
|
||||
const activeCategoryCount = categoryPages[activeCategoryPage]?.length ?? 0;
|
||||
const activeCategoryRows = Math.ceil(activeCategoryCount / 3);
|
||||
@@ -137,7 +154,6 @@ export function Home() {
|
||||
};
|
||||
|
||||
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 activeRecommendedDot = Math.min(
|
||||
recommendedDotCount - 1,
|
||||
@@ -191,7 +207,7 @@ export function Home() {
|
||||
className="h-9 w-9 shrink-0 text-ark-gold"
|
||||
/>
|
||||
<div className="w-full truncate text-[13px] font-medium leading-[19.5px] text-white">
|
||||
{c.name}
|
||||
{figmaCategoryName(c)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
@@ -231,8 +247,8 @@ export function Home() {
|
||||
</div>
|
||||
|
||||
<div className="mt-7 hidden grid-cols-3 gap-3 min-[440px]:gap-3.5 md:grid md:grid-cols-5 md:gap-3 lg:grid-cols-6 xl:grid-cols-7 xl:gap-4">
|
||||
{cats.map((c) => {
|
||||
const { line1, line2 } = categoryCardLines(c.name);
|
||||
{figmaOrderedCategories.map((c) => {
|
||||
const { line1, line2 } = categoryCardLines(figmaCategoryName(c));
|
||||
return (
|
||||
<Link
|
||||
key={c.id}
|
||||
@@ -275,7 +291,7 @@ export function Home() {
|
||||
>
|
||||
{rec.map((r, index) => (
|
||||
<div key={r.id} className="snap-start">
|
||||
<RecommendedCard r={r} visualIndex={index} />
|
||||
<RecommendedCard r={r} visualIndex={index} useFigmaDesign />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -351,31 +367,7 @@ export function Home() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
<span id="popular" className="block scroll-mt-24" aria-hidden="true" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||