"use client"; import { ArrowRightIcon, ArrowUpRightIcon } from "lucide-react"; import { motion, MotionConfig } from "motion/react"; import type { ReactNode } from "react"; import { cn } from "@/lib/utils"; export type ArticleCover = "rings" | "dots" | "waves" | "pages" | "orbit" | "glyph"; export type ArticleTone = "violet" | "sky" | "emerald" | "amber" | "rose" | "indigo"; export interface Article { title: string; excerpt?: string; href: string; tags: string[]; minutes: number; date: string; author: { name: string }; cover?: ArticleCover; tone?: ArticleTone; } export interface ArticleCardsProps { eyebrow?: string; title?: ReactNode; articles: Article[]; allHref?: string; className?: string; } const ease = [0.23, 1, 0.32, 1] as const; const tones: Record = { violet: { bg: "from-violet-100 to-violet-50/40 dark:from-violet-500/20 dark:to-violet-500/5", ink: "text-violet-500/45 dark:text-violet-300/30", chip: "bg-violet-500/10 text-violet-700 dark:text-violet-300", avatar: "bg-violet-100 text-violet-700 dark:bg-violet-950 dark:text-violet-300", }, sky: { bg: "from-sky-100 to-sky-50/40 dark:from-sky-500/20 dark:to-sky-500/5", ink: "text-sky-500/45 dark:text-sky-300/30", chip: "bg-sky-500/10 text-sky-700 dark:text-sky-300", avatar: "bg-sky-100 text-sky-700 dark:bg-sky-950 dark:text-sky-300", }, emerald: { bg: "from-emerald-100 to-emerald-50/40 dark:from-emerald-500/20 dark:to-emerald-500/5", ink: "text-emerald-500/45 dark:text-emerald-300/30", chip: "bg-emerald-500/10 text-emerald-700 dark:text-emerald-300", avatar: "bg-emerald-100 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300", }, amber: { bg: "from-amber-100 to-amber-50/40 dark:from-amber-500/20 dark:to-amber-500/5", ink: "text-amber-500/55 dark:text-amber-300/30", chip: "bg-amber-500/12 text-amber-800 dark:text-amber-300", avatar: "bg-amber-100 text-amber-800 dark:bg-amber-950 dark:text-amber-300", }, rose: { bg: "from-rose-100 to-rose-50/40 dark:from-rose-500/20 dark:to-rose-500/5", ink: "text-rose-500/45 dark:text-rose-300/30", chip: "bg-rose-500/10 text-rose-700 dark:text-rose-300", avatar: "bg-rose-100 text-rose-700 dark:bg-rose-950 dark:text-rose-300", }, indigo: { bg: "from-indigo-100 to-indigo-50/40 dark:from-indigo-500/20 dark:to-indigo-500/5", ink: "text-indigo-500/45 dark:text-indigo-300/30", chip: "bg-indigo-500/10 text-indigo-700 dark:text-indigo-300", avatar: "bg-indigo-100 text-indigo-700 dark:bg-indigo-950 dark:text-indigo-300", }, }; const toneOrder = Object.keys(tones) as ArticleTone[]; const coverOrder: ArticleCover[] = ["rings", "pages", "waves", "dots", "orbit", "glyph"]; function CoverArt({ cover, title }: { cover: ArticleCover; title: string }) { const stroke = { stroke: "currentColor", strokeWidth: 1.5, fill: "none", vectorEffect: "non-scaling-stroke" } as const; switch (cover) { case "rings": return ( {[20, 45, 70, 95, 120, 145].map((r) => ( ))} ); case "pages": return ( {[0, 1, 2].map((i) => ( ))} {[0, 1, 2, 3].map((i) => ( ))} ); case "waves": return ( {[0, 1, 2, 3, 4, 5, 6].map((i) => ( ))} ); case "dots": return ( {Array.from({ length: 9 }, (_, r) => Array.from({ length: 16 }, (_, c) => ( = 6 && c <= 10 ? 5 : 1.6} /> )), )} ); case "orbit": return ( ); default: return ( {title.trim()[0]?.toUpperCase()} {title.trim()[0]?.toLowerCase()} ); } } function initials(name: string) { return name .split(/\s+/) .map((p) => p[0]) .slice(0, 2) .join("") .toUpperCase(); } function Card({ article, index, size }: { article: Article; index: number; size: "feature" | "side" | "grid" }) { const tone = tones[article.tone ?? toneOrder[index % toneOrder.length]]; const cover = article.cover ?? coverOrder[index % coverOrder.length]; const feature = size === "feature"; return (
{article.tags.slice(0, 2).map((tag, i) => ( {tag} ))} {article.minutes} min read

{article.title}

{article.excerpt && size !== "side" && (

{article.excerpt}

)}
{initials(article.author.name)} {article.author.name}
); } export function ArticleCards({ eyebrow = "Journal", title = "Latest writing", articles, allHref, className }: ArticleCardsProps) { const [featured, ...rest] = articles; const side = rest.slice(0, 2); const grid = rest.slice(2); return (
{eyebrow &&

{eyebrow}

}

{title}

{allHref && ( All posts )}
{featured && (
)} {side.map((article, i) => ( ))}
{grid.length > 0 && (
{grid.map((article, i) => ( ))}
)} {allHref && ( All posts )}
); }