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
142 lines
5.7 KiB
TypeScript
142 lines
5.7 KiB
TypeScript
import { Bi } from '@/lib/i18n'
|
|
import type { Resume } from '@/lib/types'
|
|
import styles from './sections.module.css'
|
|
|
|
// 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}><Bi ru="Чем я занимаюсь" en="What I work with" /></h2>
|
|
<div className={styles.skills}>
|
|
{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({ en, ru }: Readonly<Pair>) {
|
|
return (
|
|
<section className={styles.section} id="experience">
|
|
<div className={styles.shell}>
|
|
<h2 className={styles.title}><Bi ru="Опыт" en="Experience" /></h2>
|
|
<div className={styles.entries}>
|
|
{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({ en, ru }: Readonly<Pair>) {
|
|
return (
|
|
<section className={styles.section} id="education">
|
|
<div className={styles.shell}>
|
|
<h2 className={styles.title}><Bi ru="Образование и языки" en="Education and languages" /></h2>
|
|
<div className={styles.columns}>
|
|
<div>
|
|
<h3 className={styles.label}><Bi ru="Образование" en="Education" /></h3>
|
|
<div className={styles.stack}>
|
|
{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}><Bi ru="Языки" en="Languages" /></h3>
|
|
<dl className={styles.languages}>
|
|
{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}><Bi ru="Интересы" en="Interests" /></h3>
|
|
<p>
|
|
<Bi
|
|
ru={`${(ru.interests?.length ? ru.interests : en.interests).join(', ')}.`}
|
|
en={`${en.interests.join(', ')}.`}
|
|
/>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
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}><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, 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}>
|
|
<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>
|
|
)
|
|
}
|