Files
astro-website/site/src/components/StandardCard.astro
2026-02-10 22:37:29 -05:00

87 lines
1.8 KiB
Plaintext

---
type Props = {
href?: string;
title: string;
summary?: string;
imageUrl?: string;
dateLabel?: string;
viewsLabel?: string;
sourceLabel: string;
isExternal?: boolean;
linkAttrs?: Record<string, any>;
mode?: "link" | "modal";
};
const {
href,
title,
summary,
imageUrl,
dateLabel,
viewsLabel,
sourceLabel,
isExternal,
linkAttrs,
mode = "link",
} = Astro.props;
function truncate(s: string, n: number) {
const t = (s || "").trim();
if (!t) return "";
if (t.length <= n) return t;
// ASCII ellipsis to avoid encoding issues in generated HTML.
return `${t.slice(0, Math.max(0, n - 3)).trimEnd()}...`;
}
const summaryText = truncate(summary || "", 180);
const Element = mode === "modal" ? "button" : "a";
const elementProps = mode === "modal"
? { type: "button", ...linkAttrs }
: {
href,
target: isExternal ? "_blank" : undefined,
rel: isExternal ? "noopener noreferrer" : undefined,
...linkAttrs
};
---
<Element
class="card"
{...elementProps}
>
<div class="card-media">
{imageUrl ? (
<div class="img-shimmer-wrap">
<img
src={imageUrl}
alt=""
loading="lazy"
decoding="async"
width="360"
height="180"
class="img-loading"
/>
</div>
) : (
<div class="card-placeholder" />
)}
</div>
<div class="card-body">
<div class="card-content">
<h3 class="card-title">{title}</h3>
{summaryText ? <p class="card-summary">{summaryText}</p> : null}
</div>
<div class="card-footer">
<span class="muted card-date">{dateLabel || ""}</span>
<span class="muted card-views" aria-hidden={viewsLabel ? undefined : "true"}>
{viewsLabel || ""}
</span>
<span class={`pill pill-${sourceLabel}`}>{sourceLabel}</span>
</div>
</div>
</Element>