feat(ui): add /cv route, site-wide lowercase, no-cookies footer
reproduces the legacy cv (previously at grenade.github.io/cv) as a react-router /cv route, fetched at runtime from the same gist. moves the lowercase aesthetic from per-element overrides to a single body- level rule so a future toggle can flip it from one place. adds a small site-wide footer noting why no cookie consent banner is shown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
91
ui/src/lib/cvDates.ts
Normal file
91
ui/src/lib/cvDates.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// Normalizes the date interval line of a CV entry. The legacy implementation
|
||||
// at cv/src/App.js:139 chained .replace() calls per month name; this collapses
|
||||
// that into a single regex pass.
|
||||
|
||||
const MONTH_ABBREV: Record<string, string> = {
|
||||
january: 'jan',
|
||||
february: 'feb',
|
||||
march: 'mar',
|
||||
april: 'apr',
|
||||
// may stays "may"
|
||||
june: 'jun',
|
||||
july: 'jul',
|
||||
august: 'aug',
|
||||
september: 'sep',
|
||||
october: 'oct',
|
||||
november: 'nov',
|
||||
december: 'dec',
|
||||
};
|
||||
|
||||
const MONTH_RE = new RegExp(
|
||||
`\\b(${Object.keys(MONTH_ABBREV).join('|')})\\b`,
|
||||
'gi',
|
||||
);
|
||||
|
||||
// Strip leading/trailing markdown # / whitespace, abbreviate months. Casing
|
||||
// is left to the body-level `text-transform: lowercase` so a future toggle can
|
||||
// flip it from a single place.
|
||||
export function normalizeInterval(line: string): string {
|
||||
return line
|
||||
.replace(/^[\s#]+|[\s#]+$/g, '')
|
||||
.replace(MONTH_RE, (m) => MONTH_ABBREV[m.toLowerCase()] ?? m);
|
||||
}
|
||||
|
||||
// Strip markdown link syntax (e.g. "[text](url)") down to just the text.
|
||||
export function stripMdLinks(s: string): string {
|
||||
return s.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
|
||||
}
|
||||
|
||||
// Parse the title / location-role / interval triple out of a CV entry's
|
||||
// markdown content. If the first line embeds a PNG URL (icon-style entry),
|
||||
// indices shift down by one.
|
||||
export interface ParsedHeader {
|
||||
// Plain text, markdown links stripped — for alt= attributes and similar.
|
||||
title: string;
|
||||
locationRole: string;
|
||||
// Markdown source with leading #s/whitespace stripped — for inline rendering
|
||||
// so [text](url) links render as proper anchors.
|
||||
titleMd: string;
|
||||
locationRoleMd: string;
|
||||
iconUrl: string | null;
|
||||
interval: string;
|
||||
}
|
||||
|
||||
export function parseEntryHeader(content: string): ParsedHeader {
|
||||
const lines = content.split('\n');
|
||||
const firstLine = lines[0] ?? '';
|
||||
const hasIcon = firstLine.includes('.png');
|
||||
const iconUrl = hasIcon
|
||||
? (firstLine.match(/https:[^ )\]]+\.png/)?.[0] ?? null)
|
||||
: null;
|
||||
|
||||
const titleLine = hasIcon ? (lines[1] ?? '') : firstLine;
|
||||
const locRoleLine = hasIcon ? (lines[2] ?? '') : (lines[1] ?? '');
|
||||
const intervalLine = hasIcon ? (lines[3] ?? '') : (lines[2] ?? '');
|
||||
|
||||
const titleMd = titleLine.replace(/^[\s#]+|[\s#]+$/g, '');
|
||||
const locationRoleMd = locRoleLine.replace(/^[\s#]+|[\s#]+$/g, '');
|
||||
|
||||
return {
|
||||
title: stripMdLinks(titleMd),
|
||||
locationRole: stripMdLinks(locationRoleMd),
|
||||
titleMd,
|
||||
locationRoleMd,
|
||||
iconUrl,
|
||||
interval: normalizeInterval(intervalLine),
|
||||
};
|
||||
}
|
||||
|
||||
// Anchor id for an entry: combines the section name and a slug of the title
|
||||
// line. Mirrors the legacy id format at cv/src/App.js:71.
|
||||
export function entryAnchorId(sectionName: string, content: string): string {
|
||||
const lines = content.split('\n');
|
||||
const firstLine = lines[0] ?? '';
|
||||
const titleLine = firstLine.includes('.png') ? (lines[1] ?? '') : firstLine;
|
||||
const slug = stripMdLinks(titleLine.replace(/^[\s#]+|[\s#]+$/g, ''))
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-');
|
||||
return `${sectionName}-${slug}`;
|
||||
}
|
||||
Reference in New Issue
Block a user