Initial frontend import
This commit is contained in:
138
src/components/RecommendedCard.tsx
Normal file
138
src/components/RecommendedCard.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Download } from "lucide-react";
|
||||
import { Link } from "react-router-dom";
|
||||
import type { Resource } from "../api";
|
||||
import { assetUrl, postJSON } from "../api";
|
||||
import { useI18n } from "../i18n";
|
||||
import { useMemo } from "react";
|
||||
import { formatDateYmd } from "../utils/format";
|
||||
import { recommendationCoverFallbacks } from "./FigmaBanner";
|
||||
|
||||
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]";
|
||||
|
||||
export function RecommendedCard({
|
||||
r,
|
||||
visualIndex = 0,
|
||||
}: {
|
||||
r: Resource;
|
||||
visualIndex?: number;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const cover = useMemo(() => {
|
||||
const original = r.coverImage || r.previewUrl;
|
||||
if (isPlaceholderAsset(original)) {
|
||||
return recommendationCoverFallbacks[
|
||||
visualIndex % recommendationCoverFallbacks.length
|
||||
];
|
||||
}
|
||||
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 {
|
||||
await postJSON(`/api/resources/${r.id}/download`, {});
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
window.open(dl, "_blank", "noopener,noreferrer");
|
||||
}}
|
||||
>
|
||||
<Download className="h-5 w-5" strokeWidth={2.2} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
||||
export function ComingSoonRecommendedCard({
|
||||
visualIndex = 0,
|
||||
}: {
|
||||
visualIndex?: number;
|
||||
}) {
|
||||
const cover =
|
||||
recommendationCoverFallbacks[
|
||||
visualIndex % recommendationCoverFallbacks.length
|
||||
];
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user