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:
+30
-20
@@ -1,13 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Bi, count, monthYear } from '@/lib/i18n'
|
||||
import { planSpans, type Span } from '@/lib/plan'
|
||||
import type { Portfolio, Project } from '@/lib/types'
|
||||
import Cover from './Cover'
|
||||
import styles from './projects.module.css'
|
||||
|
||||
const monthYear = (iso?: string) => (iso ? new Date(iso).toLocaleDateString('en', { month: 'short', year: 'numeric' }) : '')
|
||||
|
||||
// Aspect ratio of a plate for each span on desktop, used to decide whether a demo image can be cropped to fill it.
|
||||
const PLATE_RATIO: Record<Span, number> = { 2: 4 / 3, 3: 3 / 2, 4: 16 / 9, 6: 16 / 10 }
|
||||
|
||||
@@ -27,21 +26,21 @@ function Plate({ project, span }: Readonly<{ project: Project; span: Span }>) {
|
||||
<>
|
||||
{/* An image that does not match the plate is shown whole, over a blurred copy of itself. */}
|
||||
{!fits && image(styles.backdrop, { alt: '', 'aria-hidden': true, loading: 'lazy' })}
|
||||
{image(fits ? styles.cover : styles.contain, { alt: `${title}, demo`, loading: 'lazy', onError: () => setBroken(true) })}
|
||||
{image(fits ? styles.cover : styles.contain, { alt: title, loading: 'lazy', onError: () => setBroken(true) })}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Item({ project, span }: Readonly<{ project: Project; span: Span }>) {
|
||||
const title = project.title ?? project.name
|
||||
const links = [
|
||||
{ label: 'Repository', href: project.url },
|
||||
...(project.homepage ? [{ label: 'Live site', href: project.homepage }] : []),
|
||||
...(project.links ?? []),
|
||||
const links: { label: React.ReactNode; href: string }[] = [
|
||||
{ label: <Bi ru="Репозиторий" en="Repository" />, href: project.url },
|
||||
...(project.homepage ? [{ label: <Bi ru="Сайт" en="Live site" />, href: project.homepage }] : []),
|
||||
...(project.links ?? []).map((link) => ({ label: link.label, href: link.href })),
|
||||
]
|
||||
return (
|
||||
<article className={styles.item} data-span={span}>
|
||||
<a className={styles.plate} href={project.url} target="_blank" rel="noreferrer" aria-label={`${title}: open the repository`}>
|
||||
<a className={styles.plate} href={project.url} target="_blank" rel="noreferrer" aria-label={title}>
|
||||
<Plate project={project} span={span} />
|
||||
</a>
|
||||
<div className={styles.caption}>
|
||||
@@ -74,12 +73,21 @@ type Props = {
|
||||
|
||||
export default function Projects({ projects, status, generatedAt, profileUrl, portfolio }: Readonly<Props>) {
|
||||
const spans = useMemo(() => planSpans(projects.length), [projects.length])
|
||||
const profile = profileUrl.replace('https://', '')
|
||||
const total = portfolio.count ? count(portfolio.count, ['проект', 'проекта', 'проектов'], ['project', 'projects']) : null
|
||||
return (
|
||||
<section className={styles.section} id="projects">
|
||||
<div className={styles.shell}>
|
||||
<div className={styles.head}>
|
||||
<h2>Selected work</h2>
|
||||
{status === 'live' && <p className={styles.note}>Read live from Gitea, updated {monthYear(generatedAt)}</p>}
|
||||
<h2><Bi ru="Избранные проекты" en="Selected work" /></h2>
|
||||
{status === 'live' && (
|
||||
<p className={styles.note}>
|
||||
<Bi
|
||||
ru={`Данные читаются из Gitea, обновлено ${monthYear(generatedAt, 'ru')}`}
|
||||
en={`Read live from Gitea, updated ${monthYear(generatedAt, 'en')}`}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{status === 'loading' && (
|
||||
@@ -87,14 +95,12 @@ export default function Projects({ projects, status, generatedAt, profileUrl, po
|
||||
<span /><span /><span />
|
||||
</div>
|
||||
)}
|
||||
{status === 'loading' && (
|
||||
<noscript>
|
||||
<p className={styles.notice}>The project list is loaded live and needs JavaScript. See <a href={profileUrl}>{profileUrl.replace('https://', '')}</a>.</p>
|
||||
</noscript>
|
||||
)}
|
||||
{status === 'offline' && (
|
||||
<p className={styles.notice}>
|
||||
The project list could not be loaded right now. All repositories are on <a href={profileUrl}>{profileUrl.replace('https://', '')}</a>.
|
||||
<Bi
|
||||
ru={<>Список проектов сейчас не загрузился. Все репозитории лежат на <a href={profileUrl}>{profile}</a>.</>}
|
||||
en={<>The project list could not be loaded right now. All repositories are on <a href={profileUrl}>{profile}</a>.</>}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -105,14 +111,18 @@ export default function Projects({ projects, status, generatedAt, profileUrl, po
|
||||
</div>
|
||||
|
||||
{status === 'live' && projects.length > 0 && (
|
||||
<a className={styles.more} href={portfolio.href} target="_blank" rel="noreferrer">
|
||||
<a className={styles.more} href={portfolio.href}>
|
||||
<span>
|
||||
<span className={styles.moreTitle}>There is more in the portfolio</span>
|
||||
<span className={styles.moreTitle}><Bi ru="Есть ещё в портфолио" en="There is more in the portfolio" /></span>
|
||||
<span className={styles.moreText}>
|
||||
{portfolio.count ? `${portfolio.count} projects in total, including the ones above.` : 'The full list of projects, including the ones above.'}
|
||||
{total ? (
|
||||
<Bi ru={`Всего ${total.ru}, включая эти.`} en={`${total.en} in total, including the ones above.`} />
|
||||
) : (
|
||||
<Bi ru="Полный список проектов, включая эти." en="The full list of projects, including the ones above." />
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
<span className={styles.moreButton}>{portfolio.label ?? 'Open the portfolio'}</span>
|
||||
<span className={styles.moreButton}><Bi ru="Открыть портфолио" en="Open the portfolio" /></span>
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user