Files
Arkie-Library-Frontend/src/components/RecommendedCard.tsx

151 lines
5.0 KiB
TypeScript
Raw Normal View History

2026-05-16 00:18:22 +08:00
import { Download } from "lucide-react";
import { Link } from "react-router-dom";
import type { Resource } from "../api";
2026-05-26 12:07:13 +08:00
import { assetUrl, postJSON, postNoBody } from "../api";
2026-05-16 00:18:22 +08:00
import { useI18n } from "../i18n";
import { useMemo } from "react";
import { formatDateYmd } from "../utils/format";
import { officialRecommendationCoverFallbacks } from "./FigmaBanner";
import { 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 =
"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]";
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-26 12:07:13 +08:00
r: RecommendedResource;
2026-05-16 00:18:22 +08:00
visualIndex?: number;
}) {
const { t } = useI18n();
const cover = useMemo(() => {
const original = r.coverImage || r.previewUrl;
if (isPlaceholderAsset(original)) {
return officialRecommendationCoverFallbacks[
visualIndex % officialRecommendationCoverFallbacks.length
2026-05-16 00:18:22 +08:00
];
}
return assetUrl(original);
}, [r.coverImage, r.previewUrl, visualIndex]);
const dateStr = formatDateYmd(r.updatedAt);
const dl =
r.isDownloadable && (r.fileUrl || r.previewUrl)
? assetUrl(r.fileUrl || r.previewUrl)
: "";
return (
<article className={CARD_CLASS}>
<Link
to={`/resource/${r.id}`}
className="relative block aspect-[246.4/138.6] overflow-hidden bg-black"
>
{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>
<div className="flex min-h-[121px] flex-1 flex-col p-4 pt-3">
<Link
to={`/resource/${r.id}`}
className="text-base font-bold leading-snug text-white line-clamp-2 hover:text-ark-gold2"
>
{r.title}
</Link>
<div className="mt-auto flex items-center justify-between gap-2 pt-4 text-xs text-ark-muted">
<div className="min-w-0 truncate">
<span className="text-neutral-400">{r.categoryName}</span>
<span className="mx-1.5 text-ark-line">·</span>
<time dateTime={r.updatedAt}>{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"
title={t("download")}
aria-label={t("download")}
onClick={async (e) => {
e.preventDefault();
e.stopPropagation();
try {
2026-05-26 12:07:13 +08:00
if (r.downloadPostId && r.downloadAttachmentId) {
await postNoBody(
`/api/posts/${r.downloadPostId}/attachments/${r.downloadAttachmentId}/download`,
);
} else {
await postJSON(`/api/resources/${r.id}/download`, {});
}
2026-05-16 00:18:22 +08:00
} catch {
/* ignore */
}
void downloadFile(dl, r.title).catch(() => {});
2026-05-16 00:18:22 +08:00
}}
>
<Download className="h-5 w-5" strokeWidth={2.2} />
</button>
) : null}
</div>
</div>
</article>
);
}
export function ComingSoonRecommendedCard({
visualIndex = 0,
}: {
visualIndex?: number;
}) {
const cover =
officialRecommendationCoverFallbacks[
visualIndex % officialRecommendationCoverFallbacks.length
2026-05-16 00:18:22 +08:00
];
return (
<article
className={`${CARD_CLASS} cursor-default opacity-95`}
aria-label="即将到来"
>
<div className="relative block aspect-[246.4/138.6] overflow-hidden bg-black">
<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>
<div className="flex min-h-[121px] flex-1 flex-col p-4 pt-3">
<div className="text-base font-bold leading-snug text-white line-clamp-2">
</div>
<div className="mt-auto pt-4 text-xs text-ark-muted">
</div>
</div>
</article>
);
}