Make the resume bilingual and part of the shared site chrome
Adds a Russian version (data/resume.ru.json, default language) with an RU/EN switch stored under the key shared with the front page and the portfolio. The top bar becomes the site-wide one (brand, Resume, Portfolio, Gitea, Contact, language, theme) with a second row of anchors for this page's sections, and the fonts move to Unbounded and Onest so the three pages share one type system. Portfolio links now point at /portfolio/. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtcZtnG4GQA1d6mW5PNrS9
This commit is contained in:
+88
-50
@@ -1,78 +1,108 @@
|
||||
import { Bi } from '@/lib/i18n'
|
||||
import type { Resume } from '@/lib/types'
|
||||
import styles from './sections.module.css'
|
||||
|
||||
export function Skills({ resume }: Readonly<{ resume: Resume }>) {
|
||||
// The English resume gives the structure; the Russian one is paired with it by position, and anything the
|
||||
// Russian file lacks falls back to the English text.
|
||||
type Pair = { en: Resume; ru: Resume }
|
||||
|
||||
export function Skills({ en, ru }: Readonly<Pair>) {
|
||||
return (
|
||||
<section className={styles.section} id="skills">
|
||||
<div className={styles.shell}>
|
||||
<h2 className={styles.title}>What I work with</h2>
|
||||
<h2 className={styles.title}><Bi ru="Чем я занимаюсь" en="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>
|
||||
))}
|
||||
{en.skills.map((skill, i) => {
|
||||
const r = ru.skills?.[i]
|
||||
return (
|
||||
<article className={styles.skill} key={skill.title}>
|
||||
<h3><Bi ru={r?.title ?? skill.title} en={skill.title} /></h3>
|
||||
<p><Bi ru={r?.text ?? skill.text} en={skill.text} /></p>
|
||||
<ul className={styles.tags}>
|
||||
{skill.stack.map((tag, t) => (
|
||||
<li key={tag}><Bi ru={r?.stack?.[t] ?? tag} en={tag} /></li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function Experience({ resume }: Readonly<{ resume: Resume }>) {
|
||||
export function Experience({ en, ru }: Readonly<Pair>) {
|
||||
return (
|
||||
<section className={styles.section} id="experience">
|
||||
<div className={styles.shell}>
|
||||
<h2 className={styles.title}>Experience</h2>
|
||||
<h2 className={styles.title}><Bi ru="Опыт" en="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>
|
||||
))}
|
||||
{en.experience.map((entry, i) => {
|
||||
const r = ru.experience?.[i]
|
||||
return (
|
||||
<article className={styles.entry} key={entry.title}>
|
||||
<p className={styles.meta}><Bi ru={r?.meta ?? entry.meta} en={entry.meta} /></p>
|
||||
<div>
|
||||
<h3><Bi ru={r?.title ?? entry.title} en={entry.title} /></h3>
|
||||
<ul className={styles.list}>
|
||||
{entry.points.map((point, p) => (
|
||||
<li key={point}><Bi ru={r?.points?.[p] ?? point} en={point} /></li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function Education({ resume }: Readonly<{ resume: Resume }>) {
|
||||
export function Education({ en, ru }: Readonly<Pair>) {
|
||||
return (
|
||||
<section className={styles.section} id="education">
|
||||
<div className={styles.shell}>
|
||||
<h2 className={styles.title}>Education and languages</h2>
|
||||
<h2 className={styles.title}><Bi ru="Образование и языки" en="Education and languages" /></h2>
|
||||
<div className={styles.columns}>
|
||||
<div>
|
||||
<h3 className={styles.label}>Education</h3>
|
||||
<h3 className={styles.label}><Bi ru="Образование" en="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>
|
||||
))}
|
||||
{en.education.map((entry, i) => {
|
||||
const r = ru.education?.[i]
|
||||
return (
|
||||
<div key={entry.institution}>
|
||||
<p className={styles.strong}><Bi ru={r?.institution ?? entry.institution} en={entry.institution} /></p>
|
||||
<p><Bi ru={r?.program ?? entry.program} en={entry.program} /></p>
|
||||
<p className={styles.meta}><Bi ru={r?.period ?? entry.period} en={entry.period} /></p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={styles.label}>Languages</h3>
|
||||
<h3 className={styles.label}><Bi ru="Языки" en="Languages" /></h3>
|
||||
<dl className={styles.languages}>
|
||||
{resume.languages.map((language) => (
|
||||
<div key={language.name}>
|
||||
<dt>{language.name}</dt>
|
||||
<dd>{language.level}</dd>
|
||||
</div>
|
||||
))}
|
||||
{en.languages.map((language, i) => {
|
||||
const r = ru.languages?.[i]
|
||||
return (
|
||||
<div key={language.name}>
|
||||
<dt><Bi ru={r?.name ?? language.name} en={language.name} /></dt>
|
||||
<dd><Bi ru={r?.level ?? language.level} en={language.level} /></dd>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</dl>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={styles.label}>Interests</h3>
|
||||
<p>{resume.interests.join(', ')}.</p>
|
||||
<h3 className={styles.label}><Bi ru="Интересы" en="Interests" /></h3>
|
||||
<p>
|
||||
<Bi
|
||||
ru={`${(ru.interests?.length ? ru.interests : en.interests).join(', ')}.`}
|
||||
en={`${en.interests.join(', ')}.`}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,23 +110,31 @@ export function Education({ resume }: Readonly<{ resume: Resume }>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function Contact({ resume }: Readonly<{ resume: Resume }>) {
|
||||
const [primary, ...others] = resume.contacts
|
||||
export function Contact({ en, ru }: Readonly<Pair>) {
|
||||
const [primary, ...others] = en.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>
|
||||
<h2 className={styles.contactTitle}><Bi ru="Связаться" en="Get in touch" /></h2>
|
||||
<p className={styles.contactLead}><Bi ru="Почта — самый быстрый способ." en="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>
|
||||
))}
|
||||
{others.map((contact, i) => {
|
||||
const label = ru.contacts?.[i + 1]?.label ?? contact.label
|
||||
return (
|
||||
<li key={contact.value}>
|
||||
<span>{label === contact.label ? contact.label : <Bi ru={label} en={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>
|
||||
<p className={styles.legal}>
|
||||
<Bi
|
||||
ru="© 2026 Эмиль Шанаев. Сайт работает на собственном сервере: Gitea, Caddy и Docker. Резюме и проекты подтягиваются из репозиториев."
|
||||
en="© 2026 Emil Shanayev. This site runs on my own server: Gitea, Caddy and Docker. The resume and projects are read from repositories."
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user