Resume site with live content from Gitea
Replaces the static portfolio with a resume site that reads /data/resume.json: the text comes from data/resume.json in this repo, and the projects are the public Gitea repos tagged `resume`. Merges in the content of the hh.ru resume (backend/DevOps skills, experience, languages) without personal details. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,3 +102,43 @@ a:focus-visible { outline:2px solid var(--acid); outline-offset:5px; }
|
||||
.tool-grid { grid-template-columns:1fr; }.tool-grid a:nth-child(n) { border-right:0; padding:24px 0; }
|
||||
.education { grid-template-columns:1fr; gap:20px; padding-bottom:70px; }.contact-grid a { grid-template-columns:80px 1fr 20px; }.contact-grid strong { font-size:12px; overflow-wrap:anywhere; }.footer-bottom { gap:20px; }.footer-bottom span:last-child { text-align:right; }
|
||||
}
|
||||
|
||||
/* Resume site: skills, experience, live project list, side info. */
|
||||
.skill-grid { display:grid; grid-template-columns:repeat(3,1fr); }
|
||||
.skill-card { padding:34px 28px 34px 0; border-right:1px solid var(--line); border-bottom:1px solid var(--line); transition:background .25s; }
|
||||
.skill-card:nth-child(3n+2), .skill-card:nth-child(3n) { padding-left:28px; }
|
||||
.skill-card:nth-child(3n) { border-right:0; }
|
||||
.skill-card:hover { background:linear-gradient(135deg,rgba(199,255,70,.09),transparent 68%); }
|
||||
.skill-card h3 { font-size:22px; margin:30px 0 12px; letter-spacing:-.035em; }
|
||||
.skill-card p { margin:0; color:#b0bab2; font-size:14px; line-height:1.65; }
|
||||
.exp-item { display:grid; grid-template-columns:200px 1fr; gap:34px; padding:34px 0; border-bottom:1px solid var(--line); }
|
||||
.exp-item > span { font:300 11px/1.6 'DM Mono',monospace; color:var(--muted); text-transform:uppercase; letter-spacing:.05em; }
|
||||
.exp-item h3 { margin:0 0 16px; font-size:22px; letter-spacing:-.03em; }
|
||||
.exp-item ul { margin:0; padding:0; list-style:none; display:grid; gap:9px; color:#b0bab2; line-height:1.6; }
|
||||
.exp-item li::before { content:'—'; color:var(--muted); margin-right:10px; }
|
||||
.status-note { margin:0; padding:44px 0; border-bottom:1px solid var(--line); color:var(--muted); font:400 12px/1.8 'DM Mono',monospace; }
|
||||
.status-note a { color:var(--acid); border-bottom:1px solid rgba(199,255,70,.4); }
|
||||
.status-note b { color:var(--ink); font-weight:500; }
|
||||
.project-updated { margin:20px 0 0; color:var(--muted); font:300 10px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.05em; }
|
||||
.about-grid { display:grid; grid-template-columns:1.25fr 1fr; gap:64px; padding-bottom:70px; }
|
||||
.about-grid .education-list { padding-bottom:0; }
|
||||
.side-info { padding-top:18px; }
|
||||
.mini-heading { margin:0 0 14px; padding-bottom:10px; border-bottom:1px solid rgba(243,246,240,.35); font:500 11px 'DM Mono',monospace; text-transform:uppercase; letter-spacing:.11em; }
|
||||
.side-info .mini-heading:not(:first-child) { margin-top:40px; }
|
||||
.lang-list { list-style:none; margin:0; padding:0; }
|
||||
.lang-list li { display:flex; justify-content:space-between; padding:10px 0; border-bottom:1px solid var(--line); }
|
||||
.lang-list b { font:400 11px 'DM Mono',monospace; color:var(--acid); font-weight:400; }
|
||||
.side-info .tag-list { margin-top:0; }
|
||||
.contact-note { color:#92958a; font-style:normal; font-size:12px; }
|
||||
@media (max-width:980px) {
|
||||
.skill-grid { grid-template-columns:repeat(2,1fr); }
|
||||
.skill-card:nth-child(n) { padding:34px 24px 34px 0; border-right:1px solid var(--line); }
|
||||
.skill-card:nth-child(2n) { padding-left:24px; border-right:0; }
|
||||
.about-grid { grid-template-columns:1fr; gap:40px; }
|
||||
}
|
||||
@media (max-width:760px) {
|
||||
.skill-grid { grid-template-columns:1fr; }
|
||||
.skill-card:nth-child(n) { padding:30px 0; border-right:0; }
|
||||
.exp-item { grid-template-columns:1fr; gap:14px; padding:28px 0; }
|
||||
.about-grid { padding-bottom:50px; }
|
||||
}
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import type { Metadata } from 'next'
|
||||
import './globals.css'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Emil Shanayev — AI Systems & Scientific Software',
|
||||
description: 'Portfolio of Emil Shanayev: AI-native systems, scientific software, graphics and infrastructure.',
|
||||
title: 'Emil Shanayev — Resume',
|
||||
description: 'Resume of Emil Shanayev: backend, DevOps, AI systems, scientific software and game technology.',
|
||||
authors: [{ name: 'Emil Shanayev' }],
|
||||
}
|
||||
|
||||
|
||||
+4
-126
@@ -1,129 +1,7 @@
|
||||
import { resume } from '@/data/resume'
|
||||
|
||||
const ArrowIcon = () => (
|
||||
<svg className="link-arrow" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M3 13 13 3M6 3h7v7" />
|
||||
</svg>
|
||||
)
|
||||
import resume from '@/data/resume.json'
|
||||
import type { Resume } from '@/lib/types'
|
||||
import Site from './site'
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
<div className="ambient ambient-one" aria-hidden="true" />
|
||||
<div className="ambient ambient-two" aria-hidden="true" />
|
||||
<header className="nav shell">
|
||||
<a className="mark" href="#top" aria-label="Home">ES<span>/26</span></a>
|
||||
<nav aria-label="Main navigation">
|
||||
<a href="#work">Work</a>
|
||||
<a href="#profile">Profile</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className="hero shell" id="top">
|
||||
<div className="hero-kicker"><span className="pulse" /> {resume.availability}</div>
|
||||
<div className="hero-orbit" aria-hidden="true"><span>AI</span><i /></div>
|
||||
<p className="eyebrow">{resume.eyebrow}</p>
|
||||
<h1>{resume.name}</h1>
|
||||
<h2>{resume.role}</h2>
|
||||
<p className="hero-copy">{resume.bio}</p>
|
||||
<div className="hero-actions">
|
||||
<a className="button primary" href="#work">Selected work</a>
|
||||
<a className="button" href={resume.contacts[0].href} target="_blank" rel="noreferrer">GitHub <ArrowIcon /></a>
|
||||
</div>
|
||||
<div className="hero-facts">
|
||||
<span><b>05</b> featured systems</span>
|
||||
<span><b>∞</b> curiosity for hard problems</span>
|
||||
</div>
|
||||
<div className="hero-index" aria-hidden="true">01—05</div>
|
||||
</section>
|
||||
|
||||
<section className="capabilities shell" id="profile">
|
||||
<div className="section-heading">
|
||||
<p>Profile / capabilities</p>
|
||||
<span>Three intersecting domains</span>
|
||||
</div>
|
||||
<div className="cap-grid">
|
||||
{resume.capabilities.map((item) => (
|
||||
<article className="cap-card" key={item.index}>
|
||||
<span className="cap-number">{item.index}</span>
|
||||
<h3>{item.title}</h3>
|
||||
<p>{item.text}</p>
|
||||
<div className="tag-list">{item.stack.map((tag) => <span key={tag}>{tag}</span>)}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="work shell" id="work">
|
||||
<div className="section-heading">
|
||||
<p>Selected work</p>
|
||||
<span>Built systems, not placeholder concepts</span>
|
||||
</div>
|
||||
<div className="project-list">
|
||||
{resume.projects.map((project) => (
|
||||
<article className="project" key={project.number}>
|
||||
<div className="project-meta">
|
||||
<span>{project.number}</span>
|
||||
<p>{project.kind}</p>
|
||||
</div>
|
||||
<div className="project-body">
|
||||
<h3>{project.name}</h3>
|
||||
<p>{project.description}</p>
|
||||
<ul>{project.highlights.map((highlight) => <li key={highlight}>{highlight}</li>)}</ul>
|
||||
<div className="tag-list">{project.tech.map((tag) => <span key={tag}>{tag}</span>)}</div>
|
||||
</div>
|
||||
<div className="project-links">
|
||||
<a href={project.href} target="_blank" rel="noreferrer">Repository <ArrowIcon /></a>
|
||||
{'secondaryHref' in project && project.secondaryHref && (
|
||||
<a href={project.secondaryHref} target="_blank" rel="noreferrer">{'secondaryLabel' in project ? project.secondaryLabel : 'Related project'} <ArrowIcon /></a>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="tools shell">
|
||||
<div className="section-heading">
|
||||
<p>Tools & experiments</p>
|
||||
<span>Smaller utilities from active exploration</span>
|
||||
</div>
|
||||
<div className="tool-grid">
|
||||
{resume.tools.map((tool) => (
|
||||
<a href={tool.href} target="_blank" rel="noreferrer" key={tool.name}>
|
||||
<div><strong>{tool.name}</strong><p>{tool.note}</p></div><ArrowIcon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about shell">
|
||||
<div className="section-heading">
|
||||
<p>Education</p><span>Learning by building</span>
|
||||
</div>
|
||||
<div className="education-list">
|
||||
{resume.education.map((education) => (
|
||||
<div className="education" key={education.institution}>
|
||||
<span>{education.period}</span>
|
||||
<div><h3>{education.institution}</h3><p>{education.program}</p></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="footer shell" id="contact">
|
||||
<p className="eyebrow">Have a difficult technical problem?</p>
|
||||
<h2>Let’s build something<br /><em>that actually works.</em></h2>
|
||||
<div className="contact-grid">
|
||||
{resume.contacts.map((contact) => (
|
||||
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer" key={contact.label}>
|
||||
<span>{contact.label}</span><strong>{contact.value}</strong><ArrowIcon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-bottom"><span>© 2026 Emil Shanayev</span><span>Designed as a static Next.js portfolio</span></div>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
return <Site initial={resume as unknown as Resume} />
|
||||
}
|
||||
|
||||
+212
@@ -0,0 +1,212 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Live, Project, Resume } from '@/lib/types'
|
||||
|
||||
// Deployed on shanaty.ru the build points at the same-origin file; elsewhere (dev, GitHub Pages) it uses the public URL.
|
||||
const DATA_URL = process.env.NEXT_PUBLIC_DATA_URL ?? 'https://shanaty.ru/data/resume.json'
|
||||
const GITEA_PROFILE = 'https://git.shanaty.ru/Emil_Shanaty'
|
||||
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
const monthYear = (iso?: string) => (iso ? new Date(iso).toLocaleDateString('en', { month: 'short', year: 'numeric' }) : '')
|
||||
|
||||
const ArrowIcon = () => (
|
||||
<svg className="link-arrow" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M3 13 13 3M6 3h7v7" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
type Status = 'loading' | 'live' | 'offline'
|
||||
|
||||
export default function Site({ initial }: Readonly<{ initial: Resume }>) {
|
||||
const [resume, setResume] = useState<Resume>(initial)
|
||||
const [projects, setProjects] = useState<Project[]>([])
|
||||
const [generatedAt, setGeneratedAt] = useState<string>()
|
||||
const [status, setStatus] = useState<Status>('loading')
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
fetch(DATA_URL, { cache: 'no-store', signal: controller.signal })
|
||||
.then((response) => (response.ok ? response.json() : Promise.reject(new Error(`HTTP ${response.status}`))))
|
||||
.then((data: Live) => {
|
||||
if (!data?.resume || !Array.isArray(data.projects)) throw new Error('unexpected data')
|
||||
setResume(data.resume)
|
||||
setProjects(data.projects)
|
||||
setGeneratedAt(data.generated_at)
|
||||
setStatus('live')
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
if (error.name !== 'AbortError') setStatus('offline')
|
||||
})
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
|
||||
// Once the list has loaded and is empty (no repo carries the `resume` topic yet), the section is hidden entirely.
|
||||
const showProjects = status !== 'live' || projects.length > 0
|
||||
const primary = resume.contacts[0]
|
||||
const github = resume.contacts.find((contact) => contact.label === 'GitHub')
|
||||
|
||||
return (
|
||||
<main>
|
||||
<div className="ambient ambient-one" aria-hidden="true" />
|
||||
<div className="ambient ambient-two" aria-hidden="true" />
|
||||
<header className="nav shell">
|
||||
<a className="mark" href="#top" aria-label="Home">ES<span>/26</span></a>
|
||||
<nav aria-label="Main navigation">
|
||||
<a href="#skills">Skills</a>
|
||||
<a href="#experience">Experience</a>
|
||||
{showProjects && <a href="#projects">Projects</a>}
|
||||
<a href="#contact">Contact</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section className="hero shell" id="top">
|
||||
<div className="hero-kicker"><span className="pulse" /> {resume.availability}</div>
|
||||
<div className="hero-orbit" aria-hidden="true"><span>AI</span><i /></div>
|
||||
<p className="eyebrow">{resume.eyebrow}</p>
|
||||
<h1>{resume.name}</h1>
|
||||
<h2>{resume.role}</h2>
|
||||
<p className="hero-copy">{resume.bio}</p>
|
||||
<div className="hero-actions">
|
||||
{showProjects && <a className="button primary" href="#projects">Selected work</a>}
|
||||
<a className={showProjects ? 'button' : 'button primary'} href={primary.href}>Get in touch <ArrowIcon /></a>
|
||||
{github && <a className="button" href={github.href} target="_blank" rel="noreferrer">GitHub <ArrowIcon /></a>}
|
||||
</div>
|
||||
<div className="hero-facts">
|
||||
{status === 'live' && projects.length > 0 && <span><b>{pad(projects.length)}</b> projects, live from Gitea</span>}
|
||||
<span><b>{pad(resume.skills.length)}</b> areas of practice</span>
|
||||
</div>
|
||||
{status === 'live' && projects.length > 0 && <div className="hero-index" aria-hidden="true">01—{pad(projects.length)}</div>}
|
||||
</section>
|
||||
|
||||
<section className="capabilities shell" id="skills">
|
||||
<div className="section-heading">
|
||||
<p>Skills / stack</p>
|
||||
<span>Where I work</span>
|
||||
</div>
|
||||
<div className="skill-grid">
|
||||
{resume.skills.map((skill, index) => (
|
||||
<article className="skill-card" key={skill.title}>
|
||||
<span className="cap-number">{pad(index + 1)}</span>
|
||||
<h3>{skill.title}</h3>
|
||||
<p>{skill.text}</p>
|
||||
<div className="tag-list">{skill.stack.map((tag) => <span key={tag}>{tag}</span>)}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="about shell" id="experience">
|
||||
<div className="section-heading">
|
||||
<p>Experience</p>
|
||||
<span>Building, running, leading</span>
|
||||
</div>
|
||||
<div className="exp-list">
|
||||
{resume.experience.map((entry) => (
|
||||
<div className="exp-item" key={entry.title}>
|
||||
<span>{entry.meta}</span>
|
||||
<div>
|
||||
<h3>{entry.title}</h3>
|
||||
<ul>{entry.points.map((point) => <li key={point}>{point}</li>)}</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{showProjects && (
|
||||
<section className="work shell" id="projects">
|
||||
<div className="section-heading">
|
||||
<p>Selected work</p>
|
||||
<span>
|
||||
{status === 'live' && `Live from Gitea · updated ${monthYear(generatedAt)}`}
|
||||
{status === 'loading' && 'Loading from Gitea…'}
|
||||
{status === 'offline' && 'Live list unavailable'}
|
||||
</span>
|
||||
</div>
|
||||
{status === 'loading' && (
|
||||
<p className="status-note">
|
||||
Loading projects…
|
||||
<noscript> The project list is loaded live from git.shanaty.ru and needs JavaScript. Repositories: <a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>.</noscript>
|
||||
</p>
|
||||
)}
|
||||
{status === 'offline' && (
|
||||
<p className="status-note">
|
||||
The live project list could not be loaded right now. Browse the repositories directly at{' '}
|
||||
<a href={GITEA_PROFILE}>{GITEA_PROFILE.replace('https://', '')}</a>
|
||||
{github && <> or <a href={github.href}>github.com/{github.value}</a></>}.
|
||||
</p>
|
||||
)}
|
||||
<div className="project-list">
|
||||
{projects.map((project, index) => (
|
||||
<article className="project" key={project.name}>
|
||||
<div className="project-meta">
|
||||
<span>{pad(index + 1)}</span>
|
||||
<p>{project.kind ?? project.language ?? 'Repository'}</p>
|
||||
</div>
|
||||
<div className="project-body">
|
||||
<h3>{project.title ?? project.name}</h3>
|
||||
{project.description && <p>{project.description}</p>}
|
||||
{project.highlights && project.highlights.length > 0 && (
|
||||
<ul>{project.highlights.map((highlight) => <li key={highlight}>{highlight}</li>)}</ul>
|
||||
)}
|
||||
{project.tech.length > 0 && <div className="tag-list">{project.tech.map((tag) => <span key={tag}>{tag}</span>)}</div>}
|
||||
{project.updated && <p className="project-updated">Updated {monthYear(project.updated)}</p>}
|
||||
</div>
|
||||
<div className="project-links">
|
||||
<a href={project.url} target="_blank" rel="noreferrer">Repository <ArrowIcon /></a>
|
||||
{project.homepage && <a href={project.homepage} target="_blank" rel="noreferrer">Live site <ArrowIcon /></a>}
|
||||
{project.links?.map((link) => (
|
||||
<a href={link.href} target="_blank" rel="noreferrer" key={link.href}>{link.label} <ArrowIcon /></a>
|
||||
))}
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<section className="about shell" id="education">
|
||||
<div className="section-heading">
|
||||
<p>Education & more</p>
|
||||
<span>Learning by building</span>
|
||||
</div>
|
||||
<div className="about-grid">
|
||||
<div className="education-list">
|
||||
{resume.education.map((entry) => (
|
||||
<div className="education" key={entry.institution}>
|
||||
<span>{entry.period}</span>
|
||||
<div><h3>{entry.institution}</h3><p>{entry.program}</p></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="side-info">
|
||||
<h3 className="mini-heading">Languages</h3>
|
||||
<ul className="lang-list">
|
||||
{resume.languages.map((language) => (
|
||||
<li key={language.name}><span>{language.name}</span><b>{language.level}</b></li>
|
||||
))}
|
||||
</ul>
|
||||
<h3 className="mini-heading">Interests</h3>
|
||||
<div className="tag-list">{resume.interests.map((interest) => <span key={interest}>{interest}</span>)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer className="footer shell" id="contact">
|
||||
<p className="eyebrow">Have a difficult technical problem?</p>
|
||||
<h2>Let’s build something<br /><em>that actually works.</em></h2>
|
||||
<div className="contact-grid">
|
||||
{resume.contacts.map((contact) => (
|
||||
<a href={contact.href} target={contact.href.startsWith('http') ? '_blank' : undefined} rel="noreferrer" key={contact.value}>
|
||||
<span>{contact.label}</span>
|
||||
<strong>{contact.value}{contact.note && <em className="contact-note"> · {contact.note}</em>}</strong>
|
||||
<ArrowIcon />
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
<div className="footer-bottom"><span>© 2026 Emil Shanayev</span><span>Content and project list are read live from Gitea</span></div>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user