The top bar's accent button becomes Portfolio (Email stays in the hero and the contact band), the hero gets a Full portfolio button, and the project list ends with a large block linking to it. The link points at Gitea's list of repos with the portfolio topic; the sync supplies the count and the resume file can override the target and label. Also eases the display weight and tracking of the name in the dark theme. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XtcZtnG4GQA1d6mW5PNrS9
63 lines
2.7 KiB
TypeScript
63 lines
2.7 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useState } from 'react'
|
|
import type { Live, Portfolio, Project, Resume } from '@/lib/types'
|
|
import Gizmo from './Gizmo'
|
|
import Hero from './Hero'
|
|
import Projects from './Projects'
|
|
import { Contact, Education, Experience, Skills } from './Sections'
|
|
import TopBar from './TopBar'
|
|
|
|
// 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'
|
|
// Used until (or unless) the live data supplies the link, so the portfolio button is always there.
|
|
const PORTFOLIO_FALLBACK: Portfolio = { href: 'https://git.shanaty.ru/explore/repos?q=portfolio&topic=1' }
|
|
|
|
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 [portfolio, setPortfolio] = useState<Portfolio>(PORTFOLIO_FALLBACK)
|
|
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)
|
|
if (data.portfolio?.href) setPortfolio(data.portfolio)
|
|
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 repository carries the `resume` topic yet) the section is left out.
|
|
const showProjects = status !== 'live' || projects.length > 0
|
|
const mailHref = resume.contacts[0].href
|
|
|
|
return (
|
|
<>
|
|
<TopBar name={resume.name} portfolio={portfolio} showProjects={showProjects} />
|
|
<Gizmo />
|
|
<main>
|
|
<Hero resume={resume} portfolio={portfolio} mailHref={mailHref} showProjects={showProjects} />
|
|
{showProjects && <Projects projects={projects} status={status} generatedAt={generatedAt} profileUrl={GITEA_PROFILE} portfolio={portfolio} />}
|
|
<Skills resume={resume} />
|
|
<Experience resume={resume} />
|
|
<Education resume={resume} />
|
|
</main>
|
|
<Contact resume={resume} />
|
|
</>
|
|
)
|
|
}
|