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>
67 lines
2.2 KiB
TypeScript
67 lines
2.2 KiB
TypeScript
import ReactMarkdown from 'react-markdown';
|
|
import Card from 'react-bootstrap/Card';
|
|
import { type CvSectionConfig, type GistFile } from '../../api/cv';
|
|
import { entryAnchorId } from '../../lib/cvDates';
|
|
|
|
interface Props {
|
|
section: CvSectionConfig;
|
|
files: GistFile[];
|
|
}
|
|
|
|
// Pipe-delimited fields (e.g. "email | phone | github, linkedin" in the
|
|
// contact section) become one paragraph per field, so each lands on its own
|
|
// line with a paragraph gap. Within each pipe-segment, comma-separated values
|
|
// are stacked with a soft line break (markdown ` \n` -> `<br/>`) so multiple
|
|
// emails / phones / urls each get their own line at a tighter spacing.
|
|
function splitPipes(content: string): string {
|
|
return content
|
|
.split('\n')
|
|
.map((line) => {
|
|
if (!line.includes(' | ')) return line;
|
|
return line
|
|
.split(' | ')
|
|
.map((segment) =>
|
|
segment.includes(', ') ? segment.split(', ').join(' \n') : segment,
|
|
)
|
|
.join('\n\n');
|
|
})
|
|
.join('\n');
|
|
}
|
|
|
|
// Renders a single section. Each .md file becomes its own block. When
|
|
// `show_section_name` is true (e.g. experience, education) the entries are
|
|
// wrapped in cards and given anchor ids so the timeline sidebar can deep-link
|
|
// to them; otherwise (e.g. summary, contact) they render as flat markdown.
|
|
export function CvSection({ section, files }: Props) {
|
|
return (
|
|
<section id={section.name} className="cv-section mb-4">
|
|
{section.show_section_name && <h2 className="cv-section-name">{section.name}</h2>}
|
|
{files.map((file) => {
|
|
const content = section.show_section_name
|
|
? file.content
|
|
: splitPipes(file.content);
|
|
if (section.show_section_name) {
|
|
return (
|
|
<div
|
|
key={file.filename}
|
|
id={entryAnchorId(section.name, file.content)}
|
|
className="cv-entry mb-3"
|
|
>
|
|
<Card className="cv-card">
|
|
<Card.Body>
|
|
<ReactMarkdown>{content}</ReactMarkdown>
|
|
</Card.Body>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<div key={file.filename} className="cv-entry">
|
|
<ReactMarkdown>{content}</ReactMarkdown>
|
|
</div>
|
|
);
|
|
})}
|
|
</section>
|
|
);
|
|
}
|