feat: add Home, Project, and Work Index views with responsive design and SEO enhancements
Deploy Documentation / check-and-deploy (push) Successful in 20s

- Implemented HomeView with a timeline layout, featuring sections for hero, profile, projects, stack, and contact.
- Created ProjectView to display individual project details, including status, highlights, and tech stack.
- Developed WorkIndexView to list all projects with breadcrumb navigation.
- Enhanced TypeScript configuration for improved module resolution.
This commit is contained in:
LOUIS POTEVIN
2026-07-02 13:55:46 +02:00
parent 4946d2586c
commit 611af1ac67
50 changed files with 8134 additions and 2873 deletions
+215
View File
@@ -0,0 +1,215 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import Eyebrow from '../components/Eyebrow.astro';
import { Badge, Button } from '@unkn0wndo3s/nova-design-system';
import { breadcrumbLd, profilePageLd } from '../lib/seo';
import { getLocaleFromUrl, useTranslations, localizePath } from '../i18n';
import type { Localized } from '../data/projects';
const locale = getLocaleFromUrl(Astro.url);
const t = useTranslations(locale);
interface Step {
period: Localized;
role: Localized;
place: string;
detail: Localized;
}
const steps: Step[] = [
{
period: { fr: 'Sept. 2025 - Août 2026', en: 'Sept. 2025 - Aug. 2026' },
role: { fr: 'Développeur full-stack en alternance', en: 'Full-stack developer (apprenticeship)' },
place: 'Legrand France, Limoges',
detail: {
fr: "Développement du DAN, application web interne pour les ateliers de production : refonte du module de gestion des utilisateurs (Vue.js/TypeScript, export CSV) et du module de statistiques avec composants graphiques sur mesure. Back-end Node.js/Express.",
en: 'Development of DAN, an internal web app for production workshops: rework of the user management module (Vue.js/TypeScript, CSV export) and of the statistics module with custom chart components. Node.js/Express back-end.',
},
},
{
period: { fr: 'Avril - Août 2025', en: 'April - Aug. 2025' },
role: { fr: "Stage puis job d'été", en: 'Internship, then summer job' },
place: 'Legrand France, Limoges',
detail: {
fr: "Premiers travaux sur le DAN : montée en compétence sur la base de code, premières fonctionnalités livrées en production. L'expérience s'est prolongée en alternance sur la même application.",
en: 'First work on DAN: ramping up on the codebase, first features shipped to production. The experience continued as an apprenticeship on the same application.',
},
},
{
period: { fr: '2023 - 2026', en: '2023 - 2026' },
role: { fr: 'BUT MMI, parcours Développement Web (DWDI)', en: 'BUT MMI degree, Web Development track (DWDI)' },
place: 'IUT du Limousin',
detail: {
fr: "Formation en développement web et dispositifs interactifs : front-end, back-end, UX, gestion de projet. Diplôme obtenu en 2026.",
en: 'Training in web development and interactive systems: front-end, back-end, UX, project management. Graduating in 2026.',
},
},
];
interface Value {
title: Localized;
body: Localized;
}
const values: Value[] = [
{
title: { fr: 'Livrer, pas prototyper', en: 'Ship, not prototype' },
body: {
fr: 'Un projet est terminé quand il tourne en production avec sa CI. Mon design system est sur npm, mon infra héberge ce site : tout ce que je montre est réellement en service.',
en: 'A project is done when it runs in production with its CI. My design system is on npm, my infrastructure hosts this site: everything I show is actually in service.',
},
},
{
title: { fr: 'Du composant au serveur', en: 'From component to server' },
body: {
fr: "Je conçois l'interface, l'API qui la sert et la machine qui l'héberge. Cette vision complète évite les frictions entre front, back et ops.",
en: 'I design the interface, the API that serves it and the machine that hosts it. That end-to-end view removes friction between front, back and ops.',
},
},
{
title: { fr: 'Réutilisable par défaut', en: 'Reusable by default' },
body: {
fr: "Tokens, composants, pipelines : je factorise ce qui se répète. C'est ce qui m'a mené à construire Nova Design System plutôt que de copier-coller des styles.",
en: 'Tokens, components, pipelines: I factor out what repeats. That mindset is what led me to build Nova Design System instead of copy-pasting styles.',
},
},
];
const crumbs = [
{ name: t('nav.home'), url: localizePath('/', locale) },
{ name: t('nav.about'), url: localizePath('/about', locale) },
];
const pageUrl = new URL(Astro.url.pathname, Astro.site ?? 'https://louis-potevin.dev').href;
const jsonLd = [breadcrumbLd(crumbs), profilePageLd(pageUrl)];
---
<BaseLayout title={t('meta.about.title')} description={t('meta.about.description')} jsonLd={jsonLd}>
<header class="page-head">
<h1 class="page-title">{t('about.title')}</h1>
<p class="page-lead">{t('about.lead')}</p>
<p class="availability">
<Badge type="success">{t('avail.badge')}</Badge>
<span>{t('avail.line')} · {t('avail.where')}</span>
</p>
</header>
<section class="section" aria-labelledby="exp-title">
<div class="eyebrow-row"><Eyebrow index="01">{t('about.exp.eyebrow')}</Eyebrow></div>
<h2 id="exp-title" class="section-title">{t('about.exp.title')}</h2>
<ol class="timeline">
{steps.map((step) => (
<li>
<p class="timeline__period">{step.period[locale]}</p>
<h3>{step.role[locale]}</h3>
<p class="timeline__place">{step.place}</p>
<p class="timeline__detail">{step.detail[locale]}</p>
</li>
))}
</ol>
</section>
<section class="section" aria-labelledby="values-title">
<div class="eyebrow-row"><Eyebrow index="02">{t('about.values.eyebrow')}</Eyebrow></div>
<h2 id="values-title" class="section-title">{t('about.values.title')}</h2>
<ul class="values">
{values.map((value) => (
<li>
<h3>{value.title[locale]}</h3>
<p>{value.body[locale]}</p>
</li>
))}
</ul>
<div class="actions">
<Button href={localizePath('/contact', locale)} size="lg">{t('nav.cta')}</Button>
</div>
</section>
</BaseLayout>
<style lang="scss">
@use '../styles/type' as type;
.page-head {
display: grid;
gap: var(--nds-spacing-md);
padding-block: clamp(48px, 8vh, 96px) clamp(24px, 4vh, 48px);
}
.page-title { @include type.text-5xl; font-size: clamp(2rem, 5.5vw, 3.4rem); }
.page-lead {
@include type.text-lg;
font-weight: 400;
max-width: 62ch;
color: var(--nds-neutral);
}
.availability {
@include type.text-label;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--nds-spacing-sm);
color: var(--nds-neutral);
}
.timeline {
display: grid;
gap: var(--nds-spacing-xl);
margin: var(--nds-spacing-xl) 0 0;
padding: 0;
list-style: none;
li {
position: relative;
padding-left: var(--nds-spacing-xl);
border-left: var(--nds-border-width-medium) solid var(--nds-border);
&::before {
content: '';
position: absolute;
left: -7px;
top: 6px;
width: 12px;
height: 12px;
border-radius: var(--nds-radius-full);
background: var(--nds-primary);
}
}
h3 { @include type.text-xl; margin-block: var(--nds-spacing-2xs); }
}
.timeline__period {
@include type.text-label;
color: var(--nds-primary);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.timeline__place { @include type.text-label; color: var(--nds-neutral); }
.timeline__detail {
@include type.text-base;
font-size: 1rem;
margin-top: var(--nds-spacing-xs);
max-width: 64ch;
color: var(--nds-neutral);
}
.values {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
gap: var(--nds-spacing-lg);
margin: var(--nds-spacing-xl) 0 0;
padding: 0;
list-style: none;
li {
padding: var(--nds-spacing-lg);
background: color-mix(in srgb, var(--nds-surface) 88%, transparent);
border: var(--nds-border-width-thin) solid var(--nds-border);
border-radius: var(--nds-radius-lg);
}
h3 { @include type.text-xl; color: var(--nds-primary); margin-bottom: var(--nds-spacing-xs); }
p { @include type.text-base; color: var(--nds-neutral); }
}
.actions { margin-top: var(--nds-spacing-2xl); }
</style>