87 lines
1.8 KiB
Plaintext
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>
|