Skip to content

Commit 9bc75db

Browse files
committed
feat: centralize project data and add project case study pages (v0.7.4)
- add src/lib/projects.ts as the single source for all 3 projects (liflow, infralens, the automotive client site), replacing the two diverging literal arrays that lived in app/projects/page.tsx and the homepage's featured section (phase 3) - add a generic /projects/[slug] template (3 static pages) — kept deliberately light (problem/solution/result/stack/link), not the redesign plan's full case-study structure, since that would mean inventing narrative content that doesn't exist yet; phases 5/6 write that content specifically for liflow and infralens - point every "case study" link (on /projects and the homepage) at /projects/${slug} instead of the phase-3 placeholder /projects - add a status badge to /projects cards, previously missing there - close out phase 4 of the portfolio redesign roadmap
1 parent 40ca723 commit 9bc75db

10 files changed

Lines changed: 352 additions & 84 deletions

File tree

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Changelog
22

3+
## [0.7.4] — 2026-08-08
4+
5+
### Contenu
6+
7+
- **`src/lib/projects.ts`** — source unique pour les 3 projets (Liflow, InfraLens, site client automobile), remplace les deux tableaux littéraux divergents qui vivaient jusqu'ici dans `app/projects/page.tsx` et le `featuredProjects` de la homepage (Phase 3). Type `Project`, `getProject`, `getFeaturedProjects`, `statusLabel`.
8+
- **`app/projects/[slug]/page.tsx`** — template générique d'étude de cas (3 pages statiques : `/projects/liflow`, `/projects/infralens`, `/projects/specialiste-automobile`). Volontairement léger (fil d'Ariane, statut, Problème/Solution/Résultat, stack, lien produit) — la structure complète à 17 sections du plan de refonte est réservée aux Phases 5/6, une fois le contenu narratif réel écrit.
9+
- **`app/projects/page.tsx`** et **homepage** — consomment maintenant `src/lib/projects.ts` ; badge de statut ajouté sur `/projects` (absent avant) ; les liens "Étude de cas" pointent vers `/projects/${slug}` au lieu du placeholder `/projects`.
10+
11+
### Nettoyage
12+
13+
- Le tag "Perpignan" (mélangé avec la stack technique sur l'ancienne fiche du site client automobile) n'apparaît plus — le nouveau champ `technologies: string[]` ne mélange plus techno et localisation. Pas une perte de contenu, juste un tag décoratif qui n'a pas sa place dans le nouveau modèle de données.
14+
15+
### Outillage
16+
17+
- **`src/lib/__tests__/projects.test.ts`** — slugs uniques, champs requis, `getProject`/`getFeaturedProjects`.
18+
- **`app/sitemap.ts`** — entrées `/projects/${slug}` ajoutées.
19+
- **`e2e/smoke.spec.ts`** — test "a project case study page renders" ajouté (13 tests).
20+
321
## [0.7.3] — 2026-08-08
422

523
### Contenu

app/page.tsx

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,15 @@
11
import { HeroText } from "@/components/hero-text";
22
import { WorldMap } from "@/components/map/world-map";
33
import { primaryNav } from "@/lib/nav";
4+
import { getFeaturedProjects, statusLabel } from "@/lib/projects";
45
import { ExternalLink } from "lucide-react";
56
import Link from "next/link";
67

78
const altNavItems = primaryNav.filter(
89
(item) => item.href !== "/" && item.href !== "/contact",
910
);
1011

11-
const featuredProjects = [
12-
{
13-
name: "Liflow",
14-
status: "Disponible",
15-
tagline:
16-
"Une timeline familiale privée pour capturer, organiser, partager et redécouvrir les souvenirs qui comptent.",
17-
stack: ["Next.js", "TypeScript", "Mistral AI"],
18-
productUrl: "https://liflow.app",
19-
caseStudyUrl: "/projects",
20-
},
21-
{
22-
name: "InfraLens",
23-
status: "Disponible",
24-
tagline:
25-
"Un outil open source qui rassemble les principaux signaux techniques d'un site dans un rapport lisible.",
26-
stack: ["Next.js", "TypeScript", "Node.js"],
27-
productUrl: "https://infralens.dev",
28-
caseStudyUrl: "/projects",
29-
},
30-
];
12+
const featuredProjects = getFeaturedProjects();
3113

3214
const color = "#22d3ee";
3315

@@ -65,7 +47,7 @@ export default function Home() {
6547
<div className="grid gap-4 sm:grid-cols-2">
6648
{featuredProjects.map((project) => (
6749
<div
68-
key={project.name}
50+
key={project.slug}
6951
className="flex flex-col rounded-xl border p-6"
7052
style={{
7153
borderColor: `${color}20`,
@@ -76,14 +58,14 @@ export default function Home() {
7658
className="mb-3 inline-block w-fit rounded-md px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider"
7759
style={{ backgroundColor: `${color}18`, color }}
7860
>
79-
{project.status}
61+
{statusLabel(project.status)}
8062
</span>
8163
<h3 className="text-lg font-bold text-white">{project.name}</h3>
8264
<p className="mt-2 flex-1 text-sm text-zinc-400">
8365
{project.tagline}
8466
</p>
8567
<div className="mt-4 flex flex-wrap gap-1.5">
86-
{project.stack.map((tech) => (
68+
{project.technologies.map((tech) => (
8769
<span
8870
key={tech}
8971
className="rounded-md px-2 py-0.5 text-[10px] font-medium text-zinc-300"
@@ -95,19 +77,21 @@ export default function Home() {
9577
</div>
9678
<div className="mt-5 flex items-center gap-4">
9779
<Link
98-
href={project.caseStudyUrl}
80+
href={`/projects/${project.slug}`}
9981
className="text-xs font-medium text-zinc-300 hover:text-white"
10082
>
10183
Étude de cas →
10284
</Link>
103-
<a
104-
href={project.productUrl}
105-
target="_blank"
106-
rel="noopener noreferrer"
107-
className="inline-flex items-center gap-1 text-xs font-medium text-zinc-300 hover:text-white"
108-
>
109-
Voir le produit <ExternalLink size={10} />
110-
</a>
85+
{project.projectUrl && (
86+
<a
87+
href={project.projectUrl}
88+
target="_blank"
89+
rel="noopener noreferrer"
90+
className="inline-flex items-center gap-1 text-xs font-medium text-zinc-300 hover:text-white"
91+
>
92+
Voir le produit <ExternalLink size={10} />
93+
</a>
94+
)}
11195
</div>
11296
</div>
11397
))}

app/projects/[slug]/page.tsx

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { getProject, projects, statusLabel } from "@/lib/projects";
2+
import { ArrowLeft, ExternalLink } from "lucide-react";
3+
import type { Metadata } from "next";
4+
import Link from "next/link";
5+
import { notFound } from "next/navigation";
6+
7+
type Props = { params: Promise<{ slug: string }> };
8+
9+
const color = "#22d3ee";
10+
11+
export async function generateMetadata({ params }: Props): Promise<Metadata> {
12+
const { slug } = await params;
13+
const project = getProject(slug);
14+
if (!project) return {};
15+
return {
16+
title: `${project.name} — Randy Code`,
17+
description: project.tagline,
18+
};
19+
}
20+
21+
export function generateStaticParams() {
22+
return projects.map((p) => ({ slug: p.slug }));
23+
}
24+
25+
export default async function ProjectPage({ params }: Props) {
26+
const { slug } = await params;
27+
const project = getProject(slug);
28+
if (!project) notFound();
29+
30+
return (
31+
<main className="flex-1 px-6 pt-8 pb-16">
32+
<div className="mx-auto w-full max-w-3xl">
33+
{/* Breadcrumbs */}
34+
<nav
35+
aria-label="Fil d'Ariane"
36+
className="mb-4 flex items-center gap-1.5 text-xs text-zinc-500"
37+
>
38+
<Link href="/" className="hover:text-zinc-300">
39+
Accueil
40+
</Link>
41+
<span aria-hidden="true">/</span>
42+
<Link href="/projects" className="hover:text-zinc-300">
43+
Projets
44+
</Link>
45+
<span aria-hidden="true">/</span>
46+
<span aria-current="page" className="text-zinc-400">
47+
{project.name}
48+
</span>
49+
</nav>
50+
51+
<Link
52+
href="/projects"
53+
className="mb-8 inline-flex items-center gap-1.5 text-xs text-zinc-500 transition-colors hover:text-zinc-300"
54+
>
55+
<ArrowLeft size={12} />
56+
Tous les projets
57+
</Link>
58+
59+
{/* Hero */}
60+
<header className="mb-10">
61+
<span
62+
className="mb-3 inline-block w-fit rounded-md px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider"
63+
style={{ backgroundColor: `${color}18`, color }}
64+
>
65+
{statusLabel(project.status)}
66+
</span>
67+
<h1 className="text-3xl font-bold tracking-tight text-white md:text-4xl">
68+
{project.name}
69+
</h1>
70+
<p className="mt-3 max-w-xl text-base text-zinc-400">
71+
{project.tagline}
72+
</p>
73+
</header>
74+
75+
{/* Contenu */}
76+
<div className="flex flex-col gap-6">
77+
{[
78+
{ label: "Problème", text: project.problem },
79+
{ label: "Solution", text: project.solution },
80+
{ label: "Résultat", text: project.result },
81+
].map(({ label, text }) => (
82+
<section
83+
key={label}
84+
className="rounded-xl border p-6"
85+
style={{
86+
borderColor: `${color}18`,
87+
background: "oklch(0.13 0.012 252)",
88+
}}
89+
>
90+
<h2 className="mb-2 text-sm font-semibold uppercase tracking-wider text-zinc-500">
91+
{label}
92+
</h2>
93+
<p className="text-sm leading-relaxed text-zinc-300">{text}</p>
94+
</section>
95+
))}
96+
</div>
97+
98+
{/* Stack */}
99+
<div className="mt-6 flex flex-wrap items-center gap-2">
100+
<span className="text-xs text-zinc-500">Stack :</span>
101+
{project.technologies.map((tech) => (
102+
<span
103+
key={tech}
104+
className="rounded-md px-2 py-0.5 text-[10px] font-medium text-zinc-300"
105+
style={{ background: "oklch(0.18 0.012 252)" }}
106+
>
107+
{tech}
108+
</span>
109+
))}
110+
</div>
111+
112+
{/* Liens */}
113+
{project.projectUrl && (
114+
<div className="mt-8">
115+
<a
116+
href={project.projectUrl}
117+
target="_blank"
118+
rel="noopener noreferrer"
119+
className="inline-flex items-center gap-1.5 rounded-lg px-4 py-2 text-sm font-medium transition-opacity hover:opacity-80"
120+
style={{
121+
backgroundColor: `${color}18`,
122+
color,
123+
border: `1px solid ${color}30`,
124+
}}
125+
>
126+
Voir le produit <ExternalLink size={12} />
127+
</a>
128+
</div>
129+
)}
130+
</div>
131+
</main>
132+
);
133+
}

app/projects/page.tsx

Lines changed: 34 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,15 @@
11
import { PageShell } from "@/components/layout/page-shell";
2+
import { projects, statusLabel } from "@/lib/projects";
23
import { ExternalLink } from "lucide-react";
34
import type { Metadata } from "next";
5+
import Link from "next/link";
46

57
export const metadata: Metadata = {
68
title: "Projects City — Randy Code",
79
description:
810
"Projets réalisés par Randy Rimbault, développeur fullstack TypeScript. InfraLens, sites clients et applications sur mesure.",
911
};
1012

11-
const projects = [
12-
{
13-
title: "Site vitrine — Spécialiste automobile",
14-
tags: ["Next.js", "TypeScript", "SEO local", "Perpignan"],
15-
problem:
16-
"Artisan spécialisé en reprogrammation de clés, calculateurs ECU et recharge de climatisation, sans présence web pour capter des clients locaux.",
17-
solution:
18-
"Site vitrine optimisé SEO local avec pages de services dédiées, fiche Google Maps et structure pensée pour le référencement sur les requêtes métier.",
19-
result: "Site terminé, mise en ligne imminente.",
20-
link: null,
21-
},
22-
{
23-
title: "Liflow",
24-
tags: ["Next.js", "TypeScript", "Mistral AI", "Mux", "Upstash"],
25-
problem:
26-
"Aucun outil simple pour capturer et retrouver les souvenirs familiaux sans friction, sans algorithme et sans réseau social.",
27-
solution:
28-
"Application mobile-first avec timeline chronologique privée, Daily Memory automatique et récits générés par IA via Mistral.",
29-
result:
30-
"Application live et disponible sur liflow.app, en phase de croissance post-lancement.",
31-
link: "https://liflow.app",
32-
},
33-
{
34-
title: "InfraLens",
35-
tags: ["Next.js", "TypeScript", "Node.js", "Vercel"],
36-
problem:
37-
"Les développeurs et DevOps doivent jongler entre plusieurs outils pour inspecter DNS, headers HTTP et TLS d'un site.",
38-
solution:
39-
"Application web qui centralise l'analyse complète d'une URL : DNS, headers HTTP et paramètres TLS en un seul endroit.",
40-
result: "Outil utilisé au quotidien, disponible sur infralens.dev.",
41-
link: "https://infralens.dev",
42-
},
43-
];
44-
4513
export default function ProjectsPage() {
4614
return (
4715
<PageShell
@@ -54,27 +22,36 @@ export default function ProjectsPage() {
5422
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
5523
{projects.map((project) => (
5624
<article
57-
key={project.title}
25+
key={project.slug}
5826
className="flex flex-col gap-4 rounded-xl border p-6"
5927
style={{
6028
borderColor: "#22d3ee18",
6129
background: "oklch(0.13 0.012 252)",
6230
}}
6331
>
32+
<div className="flex items-center justify-between gap-2">
33+
<span
34+
className="inline-block rounded-md px-2.5 py-1 text-[10px] font-semibold uppercase tracking-wider"
35+
style={{ backgroundColor: "#22d3ee18", color: "#22d3ee" }}
36+
>
37+
{statusLabel(project.status)}
38+
</span>
39+
</div>
40+
6441
<div className="flex flex-wrap gap-1.5">
65-
{project.tags.map((tag) => (
42+
{project.technologies.map((tech) => (
6643
<span
67-
key={tag}
44+
key={tech}
6845
className="rounded-md px-2 py-0.5 text-[10px] font-medium"
6946
style={{ backgroundColor: "#22d3ee12", color: "#22d3ee" }}
7047
>
71-
{tag}
48+
{tech}
7249
</span>
7350
))}
7451
</div>
7552

7653
<h2 className="text-base font-semibold text-white">
77-
{project.title}
54+
{project.name}
7855
</h2>
7956

8057
<div className="flex flex-col gap-2.5 text-sm">
@@ -100,17 +77,26 @@ export default function ProjectsPage() {
10077
</div>
10178
</div>
10279

103-
{project.link && (
104-
<a
105-
href={project.link}
106-
target="_blank"
107-
rel="noopener noreferrer"
108-
className="mt-auto inline-flex items-center gap-1 text-xs font-medium"
80+
<div className="mt-auto flex items-center gap-4">
81+
<Link
82+
href={`/projects/${project.slug}`}
83+
className="text-xs font-medium"
10984
style={{ color: "#22d3ee" }}
11085
>
111-
Voir le projet <ExternalLink size={11} />
112-
</a>
113-
)}
86+
Étude de cas →
87+
</Link>
88+
{project.projectUrl && (
89+
<a
90+
href={project.projectUrl}
91+
target="_blank"
92+
rel="noopener noreferrer"
93+
className="inline-flex items-center gap-1 text-xs font-medium"
94+
style={{ color: "#22d3ee" }}
95+
>
96+
Voir le projet <ExternalLink size={11} />
97+
</a>
98+
)}
99+
</div>
114100
</article>
115101
))}
116102
</div>

app/sitemap.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { posts } from "@/lib/blog";
2+
import { projects } from "@/lib/projects";
23
import type { MetadataRoute } from "next";
34

45
const BASE_URL = "https://randy-code.dev";
@@ -56,5 +57,12 @@ export default function sitemap(): MetadataRoute.Sitemap {
5657
priority: 0.7,
5758
}));
5859

59-
return [...staticRoutes, ...articleRoutes];
60+
const projectRoutes: MetadataRoute.Sitemap = projects.map((project) => ({
61+
url: `${BASE_URL}/projects/${project.slug}`,
62+
lastModified: new Date(),
63+
changeFrequency: "monthly",
64+
priority: 0.7,
65+
}));
66+
67+
return [...staticRoutes, ...articleRoutes, ...projectRoutes];
6068
}

0 commit comments

Comments
 (0)