New design: studio backdrop with a perspective floor and X/Z axis lines, an orientation gizmo that orbits on scroll, Bricolage Grotesque and Instrument Sans (bundled, no build-time network). Projects are laid out as plates by a row planner that keeps every row full for any count; a demo image from the repo root is shown when present, otherwise a generated voxel sculpture. Text and projects still come live from Gitea. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtcZtnG4GQA1d6mW5PNrS9
104 lines
3.6 KiB
TypeScript
104 lines
3.6 KiB
TypeScript
import type { Resume } from '@/lib/types'
|
|
import styles from './sections.module.css'
|
|
|
|
export function Skills({ resume }: Readonly<{ resume: Resume }>) {
|
|
return (
|
|
<section className={styles.section} id="skills">
|
|
<div className={styles.shell}>
|
|
<h2 className={styles.title}>What I work with</h2>
|
|
<div className={styles.skills}>
|
|
{resume.skills.map((skill) => (
|
|
<article className={styles.skill} key={skill.title}>
|
|
<h3>{skill.title}</h3>
|
|
<p>{skill.text}</p>
|
|
<ul className={styles.tags}>{skill.stack.map((tag) => <li key={tag}>{tag}</li>)}</ul>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export function Experience({ resume }: Readonly<{ resume: Resume }>) {
|
|
return (
|
|
<section className={styles.section} id="experience">
|
|
<div className={styles.shell}>
|
|
<h2 className={styles.title}>Experience</h2>
|
|
<div className={styles.entries}>
|
|
{resume.experience.map((entry) => (
|
|
<article className={styles.entry} key={entry.title}>
|
|
<p className={styles.meta}>{entry.meta}</p>
|
|
<div>
|
|
<h3>{entry.title}</h3>
|
|
<ul className={styles.list}>{entry.points.map((point) => <li key={point}>{point}</li>)}</ul>
|
|
</div>
|
|
</article>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export function Education({ resume }: Readonly<{ resume: Resume }>) {
|
|
return (
|
|
<section className={styles.section} id="education">
|
|
<div className={styles.shell}>
|
|
<h2 className={styles.title}>Education and languages</h2>
|
|
<div className={styles.columns}>
|
|
<div>
|
|
<h3 className={styles.label}>Education</h3>
|
|
<div className={styles.stack}>
|
|
{resume.education.map((entry) => (
|
|
<div key={entry.institution}>
|
|
<p className={styles.strong}>{entry.institution}</p>
|
|
<p>{entry.program}</p>
|
|
<p className={styles.meta}>{entry.period}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<h3 className={styles.label}>Languages</h3>
|
|
<dl className={styles.languages}>
|
|
{resume.languages.map((language) => (
|
|
<div key={language.name}>
|
|
<dt>{language.name}</dt>
|
|
<dd>{language.level}</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
<div>
|
|
<h3 className={styles.label}>Interests</h3>
|
|
<p>{resume.interests.join(', ')}.</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export function Contact({ resume }: Readonly<{ resume: Resume }>) {
|
|
const [primary, ...others] = resume.contacts
|
|
return (
|
|
<footer className={styles.contact} id="contact">
|
|
<div className={styles.shell}>
|
|
<h2 className={styles.contactTitle}>Get in touch</h2>
|
|
<p className={styles.contactLead}>Email is the fastest way to reach me.</p>
|
|
<a className={styles.mail} href={primary.href}>{primary.value}</a>
|
|
<ul className={styles.contacts}>
|
|
{others.map((contact) => (
|
|
<li key={contact.value}>
|
|
<span>{contact.label}</span>
|
|
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer">{contact.value}</a>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<p className={styles.legal}>© 2026 {resume.name}. The text and the projects on this page are read live from Gitea.</p>
|
|
</div>
|
|
</footer>
|
|
)
|
|
}
|