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:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13
-8
@@ -1,3 +1,9 @@
|
||||
/* Bundled fonts (SIL Open Font License): Unbounded for headings, Onest for text; Latin and Cyrillic subsets. */
|
||||
@font-face { font-family: 'Unbounded'; font-weight: 200 900; font-display: swap; src: url('./fonts/unbounded-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
||||
@font-face { font-family: 'Unbounded'; font-weight: 200 900; font-display: swap; src: url('./fonts/unbounded-cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
||||
@font-face { font-family: 'Onest'; font-weight: 100 900; font-display: swap; src: url('./fonts/onest-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; }
|
||||
@font-face { font-family: 'Onest'; font-weight: 100 900; font-display: swap; src: url('./fonts/onest-cyrillic.woff2') format('woff2'); unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; }
|
||||
|
||||
/* Design tokens: a studio-lit 3D viewport. Axis colours (X red, Y green, Z blue) are marks and lines, not text. */
|
||||
:root {
|
||||
color-scheme: light;
|
||||
@@ -38,9 +44,6 @@
|
||||
--band-bg: #15181d;
|
||||
--band-ink: #f4f6f8;
|
||||
|
||||
/* Light type on a dark ground looks heavier, so the display weight is eased off there */
|
||||
--name-weight: 800;
|
||||
--name-tracking: -0.048em;
|
||||
|
||||
/* Derived from the palette above, so they follow the theme */
|
||||
--line: color-mix(in srgb, var(--ink) 14%, transparent);
|
||||
@@ -61,8 +64,8 @@
|
||||
--gutter: clamp(20px, 3vw, 32px);
|
||||
--section: clamp(84px, 11vw, 148px);
|
||||
|
||||
--font-body: var(--font-text), 'Segoe UI', system-ui, sans-serif;
|
||||
--font-head: var(--font-display), 'Segoe UI', system-ui, sans-serif;
|
||||
--font-body: 'Onest', 'Segoe UI', system-ui, sans-serif;
|
||||
--font-head: 'Unbounded', 'Segoe UI', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root[data-theme='dark'] {
|
||||
@@ -98,12 +101,10 @@
|
||||
--band-bg: #e8ebef;
|
||||
--band-ink: #15181d;
|
||||
|
||||
--name-weight: 700;
|
||||
--name-tracking: -0.036em;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html { background: var(--paper); scroll-behavior: smooth; scroll-padding-top: 88px; }
|
||||
html { background: var(--paper); scroll-behavior: smooth; scroll-padding-top: 124px; }
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
@@ -121,3 +122,7 @@ ul { padding: 0; list-style: none; }
|
||||
html { scroll-behavior: auto; }
|
||||
*, *::before, *::after { animation-duration: 0.01ms !important; animation-delay: 0ms !important; transition-duration: 0.01ms !important; }
|
||||
}
|
||||
|
||||
/* Bilingual text: both languages are in the markup, the html[data-lang] attribute picks one. */
|
||||
html[data-lang='ru'] [data-en],
|
||||
html[data-lang='en'] [data-ru] { display: none !important; }
|
||||
|
||||
+15
-12
@@ -1,25 +1,28 @@
|
||||
import type { Metadata } from 'next'
|
||||
import localFont from 'next/font/local'
|
||||
import './globals.css'
|
||||
|
||||
// Both fonts are bundled (SIL Open Font License), so building never depends on the network.
|
||||
const display = localFont({ src: './fonts/BricolageGrotesque-Variable.woff2', variable: '--font-display', weight: '200 800', display: 'swap' })
|
||||
const text = localFont({ src: './fonts/InstrumentSans-Variable.woff2', variable: '--font-text', weight: '400 700', display: 'swap' })
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Emil Shanayev — Resume',
|
||||
description: 'Resume of Emil Shanayev: backend, DevOps, AI systems, scientific software and game technology.',
|
||||
authors: [{ name: 'Emil Shanayev' }],
|
||||
title: 'Эмиль Шанаев — Резюме',
|
||||
description: 'Резюме Эмиля Шанаева: бэкенд, DevOps, системы на базе ИИ, научное ПО и игровые технологии.',
|
||||
authors: [{ name: 'Эмиль Шанаев' }],
|
||||
}
|
||||
|
||||
// Dark is the default theme; a stored choice of "light" is applied before the first paint so the page never flashes.
|
||||
const THEME_SCRIPT = "try{if(localStorage.getItem('theme')==='light')document.documentElement.dataset.theme='light'}catch(e){}"
|
||||
// Before the first paint: the language (the stored choice, otherwise Russian) and the theme (dark unless "light" was
|
||||
// chosen). Both keys are shared with the rest of shanaty.ru, so a choice carries over between the pages.
|
||||
const BOOT_SCRIPT = `try{var d=document.documentElement,l=localStorage.getItem('lang');if(l!=='ru'&&l!=='en')l='ru';d.dataset.lang=l;d.lang=l;if(localStorage.getItem('theme')==='light')d.dataset.theme='light';document.title=d.getAttribute('data-title-'+l)||document.title}catch(e){}`
|
||||
|
||||
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
|
||||
return (
|
||||
<html lang="en" data-theme="dark" className={`${display.variable} ${text.variable}`} suppressHydrationWarning>
|
||||
<html
|
||||
lang="ru"
|
||||
data-lang="ru"
|
||||
data-theme="dark"
|
||||
data-title-ru="Эмиль Шанаев — Резюме"
|
||||
data-title-en="Emil Shanayev — Resume"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<head>
|
||||
<script dangerouslySetInnerHTML={{ __html: THEME_SCRIPT }} />
|
||||
<script dangerouslySetInnerHTML={{ __html: BOOT_SCRIPT }} />
|
||||
</head>
|
||||
<body>{children}</body>
|
||||
</html>
|
||||
|
||||
+3
-2
@@ -1,7 +1,8 @@
|
||||
import Site from '@/components/Site'
|
||||
import resume from '@/data/resume.json'
|
||||
import resumeEn from '@/data/resume.json'
|
||||
import resumeRu from '@/data/resume.ru.json'
|
||||
import type { Resume } from '@/lib/types'
|
||||
|
||||
export default function Home() {
|
||||
return <Site initial={resume as unknown as Resume} />
|
||||
return <Site initialEn={resumeEn as unknown as Resume} initialRu={resumeRu as unknown as Resume} />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user