2026-05-27 12:23:06 +08:00
|
|
|
import { Download, LoaderCircle } from "lucide-react";
|
2026-05-16 00:18:22 +08:00
|
|
|
import { Link } from "react-router-dom";
|
|
|
|
|
import type { Resource } from "../api";
|
2026-05-28 10:53:11 +08:00
|
|
|
import { assetUrl } from "../api";
|
2026-05-16 00:18:22 +08:00
|
|
|
import { useI18n } from "../i18n";
|
2026-05-27 12:23:06 +08:00
|
|
|
import { useMemo, useState } from "react";
|
2026-05-16 00:18:22 +08:00
|
|
|
import { formatDateYmd } from "../utils/format";
|
2026-05-17 19:38:43 +08:00
|
|
|
import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
|
2026-05-27 12:17:47 +08:00
|
|
|
import {
|
|
|
|
|
downloadAttachment,
|
|
|
|
|
downloadFile,
|
|
|
|
|
} from "./messageStream/utils/downloadFile";
|
2026-05-16 00:18:22 +08:00
|
|
|
|
|
|
|
|
function isPlaceholderAsset(path: string | undefined | null) {
|
|
|
|
|
return !path || path.includes("placeholder-cover");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const CARD_CLASS =
|
2026-05-28 15:31:45 +08:00
|
|
|
"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]";
|
2026-05-16 00:18:22 +08:00
|
|
|
|
2026-05-26 12:07:13 +08:00
|
|
|
type RecommendedResource = Resource & {
|
|
|
|
|
downloadPostId?: string;
|
|
|
|
|
downloadAttachmentId?: string;
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-16 00:18:22 +08:00
|
|
|
export function RecommendedCard({
|
|
|
|
|
r,
|
|
|
|
|
visualIndex = 0,
|
2026-05-28 15:36:24 +08:00
|
|
|
useFigmaDesign = false,
|
2026-05-16 00:18:22 +08:00
|
|
|
}: {
|
2026-05-26 12:07:13 +08:00
|
|
|
r: RecommendedResource;
|
2026-05-16 00:18:22 +08:00
|
|
|
visualIndex?: number;
|
2026-05-28 15:36:24 +08:00
|
|
|
useFigmaDesign?: boolean;
|
2026-05-16 00:18:22 +08:00
|
|
|
}) {
|
|
|
|
|
const { t } = useI18n();
|
2026-05-27 12:23:06 +08:00
|
|
|
const [isDownloading, setIsDownloading] = useState(false);
|
2026-05-28 15:36:24 +08:00
|
|
|
const figmaCover =
|
|
|
|
|
officialRecommendationCoverFallbacks[
|
|
|
|
|
visualIndex % officialRecommendationCoverFallbacks.length
|
|
|
|
|
];
|
2026-05-16 00:18:22 +08:00
|
|
|
const cover = useMemo(() => {
|
2026-05-28 15:36:24 +08:00
|
|
|
if (useFigmaDesign) return figmaCover;
|
2026-05-16 00:18:22 +08:00
|
|
|
const original = r.coverImage || r.previewUrl;
|
|
|
|
|
if (isPlaceholderAsset(original)) {
|
2026-05-28 15:36:24 +08:00
|
|
|
return figmaCover;
|
2026-05-16 00:18:22 +08:00
|
|
|
}
|
|
|
|
|
return assetUrl(original);
|
2026-05-28 15:36:24 +08:00
|
|
|
}, [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;
|
2026-05-16 00:18:22 +08:00
|
|
|
|
|
|
|
|
const dl =
|
|
|
|
|
r.isDownloadable && (r.fileUrl || r.previewUrl)
|
|
|
|
|
? assetUrl(r.fileUrl || r.previewUrl)
|
|
|
|
|
: "";
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<article className={CARD_CLASS}>
|
|
|
|
|
<Link
|
|
|
|
|
to={`/resource/${r.id}`}
|
2026-05-28 15:31:45 +08:00
|
|
|
className="relative block h-[108px] overflow-hidden bg-black md:aspect-[246.4/138.6] md:h-auto"
|
2026-05-16 00:18:22 +08:00
|
|
|
>
|
|
|
|
|
{cover ? (
|
|
|
|
|
<img
|
|
|
|
|
src={cover}
|
|
|
|
|
alt=""
|
|
|
|
|
className="h-full w-full object-cover transition duration-300 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>
|
2026-05-28 15:31:45 +08:00
|
|
|
<div className="flex min-h-[131px] flex-1 flex-col p-4 pt-3 md:min-h-[121px]">
|
2026-05-16 00:18:22 +08:00
|
|
|
<Link
|
|
|
|
|
to={`/resource/${r.id}`}
|
2026-05-28 15:31:45 +08:00
|
|
|
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"
|
2026-05-16 00:18:22 +08:00
|
|
|
>
|
2026-05-28 15:36:24 +08:00
|
|
|
{displayTitle}
|
2026-05-16 00:18:22 +08:00
|
|
|
</Link>
|
2026-05-28 15:31:45 +08:00
|
|
|
<div className="mt-auto flex items-center justify-between gap-2 pt-4 text-[12px] leading-[17.38px] text-ark-muted">
|
2026-05-16 00:18:22 +08:00
|
|
|
<div className="min-w-0 truncate">
|
2026-05-28 15:36:24 +08:00
|
|
|
<span className="text-neutral-400">{displayCategoryName}</span>
|
2026-05-16 00:18:22 +08:00
|
|
|
<span className="mx-1.5 text-ark-line">·</span>
|
2026-05-28 15:36:24 +08:00
|
|
|
<time dateTime={dateTime}>{dateStr}</time>
|
2026-05-16 00:18:22 +08:00
|
|
|
</div>
|
|
|
|
|
{dl ? (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
2026-05-27 12:23:06 +08:00
|
|
|
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}
|
2026-05-16 00:18:22 +08:00
|
|
|
onClick={async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
e.stopPropagation();
|
2026-05-27 12:23:06 +08:00
|
|
|
if (isDownloading) return;
|
|
|
|
|
setIsDownloading(true);
|
2026-05-16 00:18:22 +08:00
|
|
|
try {
|
2026-05-27 12:23:06 +08:00
|
|
|
if (r.downloadPostId && r.downloadAttachmentId) {
|
|
|
|
|
await downloadAttachment(
|
|
|
|
|
r.downloadPostId,
|
|
|
|
|
r.downloadAttachmentId,
|
2026-05-28 15:36:24 +08:00
|
|
|
displayTitle,
|
2026-05-27 12:23:06 +08:00
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-28 15:36:24 +08:00
|
|
|
await downloadFile(dl, displayTitle);
|
2026-05-16 00:18:22 +08:00
|
|
|
} catch {
|
|
|
|
|
/* ignore */
|
2026-05-27 12:23:06 +08:00
|
|
|
} finally {
|
|
|
|
|
setIsDownloading(false);
|
2026-05-16 00:18:22 +08:00
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
2026-05-27 12:23:06 +08:00
|
|
|
{isDownloading ? (
|
|
|
|
|
<LoaderCircle
|
|
|
|
|
className="h-5 w-5 animate-spin"
|
|
|
|
|
strokeWidth={2.2}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<Download className="h-5 w-5" strokeWidth={2.2} />
|
|
|
|
|
)}
|
2026-05-16 00:18:22 +08:00
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function ComingSoonRecommendedCard({
|
|
|
|
|
visualIndex = 0,
|
|
|
|
|
}: {
|
|
|
|
|
visualIndex?: number;
|
|
|
|
|
}) {
|
|
|
|
|
const cover =
|
2026-05-17 19:38:43 +08:00
|
|
|
officialRecommendationCoverFallbacks[
|
|
|
|
|
visualIndex % officialRecommendationCoverFallbacks.length
|
2026-05-16 00:18:22 +08:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<article
|
|
|
|
|
className={`${CARD_CLASS} cursor-default opacity-95`}
|
|
|
|
|
aria-label="即将到来"
|
|
|
|
|
>
|
2026-05-28 15:31:45 +08:00
|
|
|
<div className="relative block h-[108px] overflow-hidden bg-black md:aspect-[246.4/138.6] md:h-auto">
|
2026-05-16 00:18:22 +08:00
|
|
|
<img
|
|
|
|
|
src={cover}
|
|
|
|
|
alt=""
|
|
|
|
|
className="h-full w-full object-cover opacity-75 grayscale-[15%]"
|
|
|
|
|
loading="lazy"
|
|
|
|
|
/>
|
|
|
|
|
<span className="absolute left-3 top-3 rounded-md bg-ark-gold px-2.5 py-1 text-xs font-semibold text-black">
|
|
|
|
|
即将到来
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
2026-05-28 15:31:45 +08:00
|
|
|
<div className="flex min-h-[131px] flex-1 flex-col p-4 pt-3 md:min-h-[121px]">
|
|
|
|
|
<div className="text-[15px] font-semibold leading-[21.72px] text-white line-clamp-2 md:text-base md:font-bold md:leading-snug">
|
2026-05-16 00:18:22 +08:00
|
|
|
即将到来
|
|
|
|
|
</div>
|
|
|
|
|
<div className="mt-auto pt-4 text-xs text-ark-muted">
|
|
|
|
|
更多内容准备中
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</article>
|
|
|
|
|
);
|
|
|
|
|
}
|