Files
Emil_Shanayev_Resume/app/site.tsx
T
EmilandClaude Sonnet 5 32c57a5ada
Deploy to GitHub Pages / build (push) Canceled after 0s
Deploy to GitHub Pages / deploy (push) Canceled after 0s
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>
2026-09-20 20:55:05 +03:00

213 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}