feat: align home category and recommendation figma assets
|
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({
|
export function RecommendedCard({
|
||||||
r,
|
r,
|
||||||
visualIndex = 0,
|
visualIndex = 0,
|
||||||
|
useFigmaDesign = false,
|
||||||
}: {
|
}: {
|
||||||
r: RecommendedResource;
|
r: RecommendedResource;
|
||||||
visualIndex?: number;
|
visualIndex?: number;
|
||||||
|
useFigmaDesign?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
const figmaCover =
|
||||||
|
officialRecommendationCoverFallbacks[
|
||||||
|
visualIndex % officialRecommendationCoverFallbacks.length
|
||||||
|
];
|
||||||
const cover = useMemo(() => {
|
const cover = useMemo(() => {
|
||||||
|
if (useFigmaDesign) return figmaCover;
|
||||||
const original = r.coverImage || r.previewUrl;
|
const original = r.coverImage || r.previewUrl;
|
||||||
if (isPlaceholderAsset(original)) {
|
if (isPlaceholderAsset(original)) {
|
||||||
return officialRecommendationCoverFallbacks[
|
return figmaCover;
|
||||||
visualIndex % officialRecommendationCoverFallbacks.length
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
return assetUrl(original);
|
return assetUrl(original);
|
||||||
}, [r.coverImage, r.previewUrl, visualIndex]);
|
}, [figmaCover, r.coverImage, r.previewUrl, useFigmaDesign]);
|
||||||
const dateStr = formatDateYmd(r.updatedAt);
|
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 =
|
const dl =
|
||||||
r.isDownloadable && (r.fileUrl || r.previewUrl)
|
r.isDownloadable && (r.fileUrl || r.previewUrl)
|
||||||
@@ -75,13 +85,13 @@ export function RecommendedCard({
|
|||||||
to={`/resource/${r.id}`}
|
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"
|
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>
|
</Link>
|
||||||
<div className="mt-auto flex items-center justify-between gap-2 pt-4 text-[12px] leading-[17.38px] 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">{displayCategoryName}</span>
|
||||||
<span className="mx-1.5 text-ark-line">·</span>
|
<span className="mx-1.5 text-ark-line">·</span>
|
||||||
<time dateTime={r.updatedAt}>{dateStr}</time>
|
<time dateTime={dateTime}>{dateStr}</time>
|
||||||
</div>
|
</div>
|
||||||
{dl ? (
|
{dl ? (
|
||||||
<button
|
<button
|
||||||
@@ -101,11 +111,11 @@ export function RecommendedCard({
|
|||||||
await downloadAttachment(
|
await downloadAttachment(
|
||||||
r.downloadPostId,
|
r.downloadPostId,
|
||||||
r.downloadAttachmentId,
|
r.downloadAttachmentId,
|
||||||
r.title,
|
displayTitle,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await downloadFile(dl, r.title);
|
await downloadFile(dl, displayTitle);
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -20,14 +20,35 @@ import {
|
|||||||
} from "../../utils/postResourceAdapter";
|
} from "../../utils/postResourceAdapter";
|
||||||
import type { Post } from "../../types/post";
|
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() {
|
export function Home() {
|
||||||
const { t, lang } = useI18n();
|
const { t, lang } = useI18n();
|
||||||
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);
|
||||||
@@ -44,9 +65,8 @@ 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`),
|
|
||||||
])
|
])
|
||||||
.then(([c, r, l, p]) => {
|
.then(([c, r, l]) => {
|
||||||
setCats(itemsOrEmpty(c));
|
setCats(itemsOrEmpty(c));
|
||||||
setRec(
|
setRec(
|
||||||
itemsOrEmpty(r.items).map((post) =>
|
itemsOrEmpty(r.items).map((post) =>
|
||||||
@@ -60,13 +80,6 @@ export function Home() {
|
|||||||
postToResource(post, lang, itemsOrEmpty(c)),
|
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)));
|
.catch((e) => setErr(String(e)));
|
||||||
}, [lang]);
|
}, [lang]);
|
||||||
@@ -74,9 +87,13 @@ export function Home() {
|
|||||||
const iconKeyForResource = (r: PostBackedResource) =>
|
const iconKeyForResource = (r: PostBackedResource) =>
|
||||||
cats.find((c) => c.id === r.categoryId)?.iconKey ?? "folder";
|
cats.find((c) => c.id === r.categoryId)?.iconKey ?? "folder";
|
||||||
|
|
||||||
|
const figmaOrderedCategories = [...cats].sort(
|
||||||
|
(a, b) => figmaCategoryRank(a) - figmaCategoryRank(b),
|
||||||
|
);
|
||||||
|
|
||||||
const categoryPages: Category[][] = [];
|
const categoryPages: Category[][] = [];
|
||||||
for (let index = 0; index < cats.length; index += 9) {
|
for (let index = 0; index < figmaOrderedCategories.length; index += 9) {
|
||||||
categoryPages.push(cats.slice(index, index + 9));
|
categoryPages.push(figmaOrderedCategories.slice(index, index + 9));
|
||||||
}
|
}
|
||||||
const activeCategoryCount = categoryPages[activeCategoryPage]?.length ?? 0;
|
const activeCategoryCount = categoryPages[activeCategoryPage]?.length ?? 0;
|
||||||
const activeCategoryRows = Math.ceil(activeCategoryCount / 3);
|
const activeCategoryRows = Math.ceil(activeCategoryCount / 3);
|
||||||
@@ -137,7 +154,6 @@ export function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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,
|
||||||
@@ -191,7 +207,7 @@ export function Home() {
|
|||||||
className="h-9 w-9 shrink-0 text-ark-gold"
|
className="h-9 w-9 shrink-0 text-ark-gold"
|
||||||
/>
|
/>
|
||||||
<div className="w-full truncate text-[13px] font-medium leading-[19.5px] text-white">
|
<div className="w-full truncate text-[13px] font-medium leading-[19.5px] text-white">
|
||||||
{c.name}
|
{figmaCategoryName(c)}
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
@@ -231,8 +247,8 @@ export function Home() {
|
|||||||
</div>
|
</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">
|
<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) => {
|
{figmaOrderedCategories.map((c) => {
|
||||||
const { line1, line2 } = categoryCardLines(c.name);
|
const { line1, line2 } = categoryCardLines(figmaCategoryName(c));
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={c.id}
|
key={c.id}
|
||||||
@@ -275,7 +291,7 @@ export function Home() {
|
|||||||
>
|
>
|
||||||
{rec.map((r, index) => (
|
{rec.map((r, index) => (
|
||||||
<div key={r.id} className="snap-start">
|
<div key={r.id} className="snap-start">
|
||||||
<RecommendedCard r={r} visualIndex={index} />
|
<RecommendedCard r={r} visualIndex={index} useFigmaDesign />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -351,31 +367,7 @@ export function Home() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="popular" className="scroll-mt-24">
|
<span id="popular" className="block scroll-mt-24" aria-hidden="true" />
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||