'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' // 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 = { 2: 4 / 3, 3: 3 / 2, 4: 16 / 9, 6: 16 / 10 } function Plate({ project, span }: Readonly<{ project: Project; span: Span }>) { const [broken, setBroken] = useState(false) const demo = project.demo const title = project.title ?? project.name if (!demo || broken) return const drift = Math.abs(Math.log(demo.width / demo.height / PLATE_RATIO[span])) const fits = drift < Math.log(1.7) const image = (className: string, extra: React.ImgHTMLAttributes) => ( // eslint-disable-next-line @next/next/no-img-element ) return ( <> {/* 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, loading: 'lazy', onError: () => setBroken(true) })} ) } function Item({ project, span }: Readonly<{ project: Project; span: Span }>) { const title = project.title ?? project.name const links: { label: React.ReactNode; href: string }[] = [ { label: , href: project.url }, ...(project.homepage ? [{ label: , href: project.homepage }] : []), ...(project.links ?? []).map((link) => ({ label: link.label, href: link.href })), ] return (

{title}

{(project.kind ?? project.language) &&

{project.kind ?? project.language}

} {project.description &&

{project.description}

} {project.highlights && project.highlights.length > 0 && (
    {project.highlights.map((point) =>
  • {point}
  • )}
)}
{project.tech.length > 0 &&
    {project.tech.map((tag) =>
  • {tag}
  • )}
}

{links.map((link) => ( {link.label} ))}

) } type Props = { projects: Project[] status: 'loading' | 'live' | 'offline' generatedAt?: string profileUrl: string portfolio: Portfolio } export default function Projects({ projects, status, generatedAt, profileUrl, portfolio }: Readonly) { const spans = useMemo(() => planSpans(projects.length), [projects.length]) const profile = profileUrl.replace('https://', '') const total = portfolio.count ? count(portfolio.count, ['проект', 'проекта', 'проектов'], ['project', 'projects']) : null return (

{status === 'live' && (

)}
{status === 'loading' && ( )} {status === 'offline' && (

Список проектов сейчас не загрузился. Все репозитории лежат на {profile}.} en={<>The project list could not be loaded right now. All repositories are on {profile}.} />

)}
{projects.map((project, index) => ( ))}
{status === 'live' && projects.length > 0 && ( {total ? ( ) : ( )} )}
) }