Post card
Date block, title, authors and summary.
Code
Copy into src/components/ph/ .
dates.ts
const asDate = (d: Date | string) => (typeof d === "string" ? new Date(d) : d);
export function dateBlock(d: Date | string) {
const date = asDate(d);
return {
month: date.toLocaleDateString("en-US", { month: "short", timeZone: "UTC" }).toUpperCase(),
dayYear: `${date.getUTCDate()}, ${date.getUTCFullYear()}`,
iso: date.toISOString(),
};
}
export function longDate(d: Date | string) {
const date = asDate(d);
return {
label: date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" }),
iso: date.toISOString(),
};
}
PostCard.astro
---
import Author from "./Author.astro";
import Icon from "./Icon.astro";
import { dateBlock } from "./dates";
type Props = {
title: string;
href: string;
date: Date | string;
description?: string;
authors?: { name: string; avatar?: string }[];
class?: string;
};
const { title, href, date, description, authors = [], class: className } = Astro.props;
const { month, dayYear, iso } = dateBlock(date);
const external = /^https?:\/\//.test(href);
const host = external ? new URL(href).hostname : null;
---
<a
href={href}
target={external ? "_blank": undefined}
rel={external ? "noopener noreferrer": undefined}
class:list={[
"flex gap-4 border border-foreground bg-card p-4 text-card-foreground no-underline transition-transform duration-150 ease-quint active:scale-[0.99] sm:gap-5 sm:p-5",
className,
]}
>
<div class="flex shrink-0 flex-col">
<time
datetime={iso}
class="flex w-16 flex-col items-center justify-center overflow-hidden bg-foreground px-2 py-1.5 font-pixel leading-none text-background uppercase sm:w-20 dark:bg-ph-yellow dark:text-ph-black"
>
<span class="text-2xl tracking-tight sm:text-3xl">{month}</span>
<span class="mt-1 text-[10px] tracking-wide whitespace-nowrap sm:text-[11px]">{dayYear}</span>
</time>
{host && <span class="mt-1 font-pixel text-[10px] tracking-wide break-all uppercase">{host}</span>}
</div>
<div class="flex min-w-0 flex-1 flex-col gap-2">
<h3 class="flex items-start gap-2 font-pixel text-2xl leading-[1.05] uppercase sm:text-3xl">
<span>{title}</span>
{external && <Icon name="external-link" class="mt-1 size-4"/>}
</h3>
{
authors.length > 0 && (
<div class="flex flex-wrap gap-x-3 gap-y-1">
{authors.map((a) => (
<Author name={a.name} avatar={a.avatar} size="sm"/>
))}
</div>
)
}
{description && <p class="line-clamp-2 text-sm">{description}</p>}
</div>
</a>
Copy into components/ph/ (or src/components/ph/).
dates.ts
const asDate = (d: Date | string) => (typeof d === "string" ? new Date(d) : d);
export function dateBlock(d: Date | string) {
const date = asDate(d);
return {
month: date.toLocaleDateString("en-US", { month: "short", timeZone: "UTC" }).toUpperCase(),
dayYear: `${date.getUTCDate()}, ${date.getUTCFullYear()}`,
iso: date.toISOString(),
};
}
export function longDate(d: Date | string) {
const date = asDate(d);
return {
label: date.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", timeZone: "UTC" }),
iso: date.toISOString(),
};
}
post-card.tsx
import { Author } from "./author";
import { cn } from "./cn";
import { dateBlock } from "./dates";
import { Icon } from "./icon";
type PostCardProps = {
title: string;
href: string;
date: Date | string;
description?: string;
authors?: { name: string; avatar?: string }[];
className?: string;
};
export function PostCard({ title, href, date, description, authors = [], className }: PostCardProps) {
const { month, dayYear, iso } = dateBlock(date);
const external = /^https?:\/\//.test(href);
const host = external ? new URL(href).hostname : null;
return (
<a
href={href}
target={external ? "_blank": undefined}
rel={external ? "noopener noreferrer": undefined}
className={cn(
"flex gap-4 border border-foreground bg-card p-4 text-card-foreground no-underline transition-transform duration-150 ease-quint active:scale-[0.99] sm:gap-5 sm:p-5",
className,
)}
>
<div className="flex shrink-0 flex-col">
<time
dateTime={iso}
className="flex w-16 flex-col items-center justify-center overflow-hidden bg-foreground px-2 py-1.5 font-pixel leading-none text-background uppercase sm:w-20 dark:bg-ph-yellow dark:text-ph-black"
>
<span className="text-2xl tracking-tight sm:text-3xl">{month}</span>
<span className="mt-1 text-[10px] tracking-wide whitespace-nowrap sm:text-[11px]">{dayYear}</span>
</time>
{host && <span className="mt-1 font-pixel text-[10px] tracking-wide break-all uppercase">{host}</span>}
</div>
<div className="flex min-w-0 flex-1 flex-col gap-2">
<h3 className="flex items-start gap-2 font-pixel text-2xl leading-[1.05] uppercase sm:text-3xl">
<span>{title}</span>
{external && <Icon name="external-link" className="mt-1 size-4"/>}
</h3>
{authors.length > 0 && (
<div className="flex flex-wrap gap-x-3 gap-y-1">
{authors.map((a) => (
<Author key={a.name} name={a.name} avatar={a.avatar} size="sm"/>
))}
</div>
)}
{description && <p className="line-clamp-2 text-sm">{description}</p>}
</div>
</a>
);
}
cn.ts
export const cn = (...parts: (string | false | null | undefined)[]) => parts.filter(Boolean).join(" ");