155 lines
3.9 KiB
TypeScript
155 lines
3.9 KiB
TypeScript
import Link from "next/link";
|
|
import {
|
|
Children,
|
|
isValidElement,
|
|
type ComponentPropsWithoutRef,
|
|
type ReactNode,
|
|
} from "react";
|
|
import { Badge } from "~/components/ui/badge";
|
|
import { Button } from "~/components/ui/button";
|
|
import { cn } from "~/lib/utils";
|
|
|
|
type CalloutVariant = "note" | "tip" | "warning";
|
|
|
|
const calloutStyles: Record<CalloutVariant, string> = {
|
|
note: "border-sky-500/40 bg-sky-500/10 text-sky-950 dark:text-sky-100",
|
|
tip: "border-emerald-500/40 bg-emerald-500/10 text-emerald-950 dark:text-emerald-100",
|
|
warning: "border-amber-500/40 bg-amber-500/10 text-amber-950 dark:text-amber-100",
|
|
};
|
|
|
|
function Callout({
|
|
title,
|
|
variant = "note",
|
|
children,
|
|
}: {
|
|
title?: string;
|
|
variant?: CalloutVariant;
|
|
children: ReactNode;
|
|
}) {
|
|
return (
|
|
<aside className={cn("my-6 rounded-md border px-4 py-3", calloutStyles[variant])}>
|
|
{title && <p className="mb-2 font-semibold">{title}</p>}
|
|
<div className="[&>*:first-child]:mt-0 [&>*:last-child]:mb-0">{children}</div>
|
|
</aside>
|
|
);
|
|
}
|
|
|
|
function Lead({ children }: { children: ReactNode }) {
|
|
return <span className="text-muted-foreground my-6 block text-lg leading-8">{children}</span>;
|
|
}
|
|
|
|
function TagList({ tags }: { tags: string[] }) {
|
|
return (
|
|
<div className="my-4 flex flex-wrap gap-1.5">
|
|
{tags.map((tag) => (
|
|
<Badge key={tag} variant="outline">
|
|
{tag}
|
|
</Badge>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ButtonLink({
|
|
href,
|
|
children,
|
|
variant = "default",
|
|
}: {
|
|
href: string;
|
|
children: ReactNode;
|
|
variant?: ComponentPropsWithoutRef<typeof Button>["variant"];
|
|
}) {
|
|
const isExternal = /^https?:\/\//.test(href);
|
|
|
|
return (
|
|
<Button asChild variant={variant}>
|
|
{isExternal ? (
|
|
<a href={href} target="_blank" rel="noreferrer">
|
|
{children}
|
|
</a>
|
|
) : (
|
|
<Link href={href}>{children}</Link>
|
|
)}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
function Figure({
|
|
src,
|
|
alt,
|
|
caption,
|
|
}: {
|
|
src: string;
|
|
alt: string;
|
|
caption?: string;
|
|
}) {
|
|
return (
|
|
<figure className="my-8">
|
|
<img src={src} alt={alt} className="w-full rounded-md border object-cover" />
|
|
{caption && <figcaption className="text-muted-foreground mt-2 text-center text-sm">{caption}</figcaption>}
|
|
</figure>
|
|
);
|
|
}
|
|
|
|
// A bare markdown image () fills the prose width. Pass a numeric
|
|
// markdown title —  — to render it as a fixed-size, circular
|
|
// avatar instead (used by the CV header). Untitled images keep the old look.
|
|
function Img({ src, alt, title }: { src: string; alt?: string; title?: string }) {
|
|
const size = title && /^\d+$/.test(title) ? Number(title) : undefined;
|
|
if (size) {
|
|
return (
|
|
<img
|
|
src={src}
|
|
alt={alt ?? ""}
|
|
width={size}
|
|
height={size}
|
|
style={{ width: size, height: size }}
|
|
className="mx-auto !my-0 shrink-0 rounded-full object-cover ring-2 ring-foreground/10"
|
|
/>
|
|
);
|
|
}
|
|
return <img src={src} alt={alt ?? ""} className="w-full rounded-md border object-cover" />;
|
|
}
|
|
|
|
function PullQuote({ children }: { children: ReactNode }) {
|
|
return (
|
|
<blockquote className="border-primary my-8 border-l-4 pl-5 text-xl leading-8 font-medium">
|
|
{children}
|
|
</blockquote>
|
|
);
|
|
}
|
|
|
|
function ExternalLink(props: ComponentPropsWithoutRef<"a">) {
|
|
const href = props.href ?? "";
|
|
const isExternal = /^https?:\/\//.test(href);
|
|
|
|
if (!isExternal) return <a {...props} />;
|
|
|
|
return <a {...props} target="_blank" rel="noreferrer" />;
|
|
}
|
|
|
|
const blockComponents = new Set<unknown>([Callout, Figure, PullQuote, TagList]);
|
|
|
|
function Paragraph({ children }: { children: ReactNode }) {
|
|
const containsBlockComponent = Children.toArray(children).some(
|
|
(child) => isValidElement(child) && blockComponents.has(child.type),
|
|
);
|
|
|
|
if (containsBlockComponent) return <>{children}</>;
|
|
|
|
return <p>{children}</p>;
|
|
}
|
|
|
|
export const mdxComponents = {
|
|
a: ExternalLink,
|
|
p: Paragraph,
|
|
Badge,
|
|
ButtonLink,
|
|
Callout,
|
|
Figure,
|
|
img: Img,
|
|
Lead,
|
|
PullQuote,
|
|
TagList,
|
|
};
|