mirror of
https://github.com/Ladebeze66/devsite.git
synced 2026-05-11 16:56:26 +02:00
etape4ok
This commit is contained in:
parent
7a42786648
commit
20d4c78df8
@ -1,19 +1,25 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const [count, setCount] = useState(0);
|
const [visitCount, setVisitCount] = useState(0);
|
||||||
|
|
||||||
function handleClick() {
|
useEffect(() => {
|
||||||
setCount(count + 1);
|
const visits = localStorage.getItem("visitCount");
|
||||||
}
|
const newVisitCount = visits ? parseInt(visits, 10) + 1 : 1;
|
||||||
|
localStorage.setItem("visitCount", newVisitCount.toString());
|
||||||
|
setVisitCount(newVisitCount);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="min-h-[80px] w-full min-w-0 rounded-lg bg-white/50 backdrop-blur">
|
<footer className="min-h-[80px] w-full min-w-0 rounded-lg bg-white/50 backdrop-blur">
|
||||||
<div className="mx-auto flex max-w-4xl min-w-0 flex-col items-center px-4 py-6 font-headline text-sm text-gray-700">
|
<div className="mx-auto flex max-w-4xl min-w-0 flex-col items-center gap-1 px-4 py-6 font-headline text-sm text-gray-700">
|
||||||
<p>© {new Date().getFullYear()} Gras-Calvet Fernand</p>
|
<p>© {new Date().getFullYear()} Gras-Calvet Fernand</p>
|
||||||
|
<p className="text-[10px] uppercase tracking-[0.3em] text-outline">
|
||||||
|
Visite n° {visitCount}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,68 +1,100 @@
|
|||||||
import { useEffect } from "react";
|
"use client";
|
||||||
import { createPortal } from "react-dom";
|
|
||||||
import CarouselCompetences from "./CarouselCompetences";
|
import { useEffect } from "react";
|
||||||
import { getApiUrl } from "../utils/getApiUrl";
|
import { createPortal } from "react-dom";
|
||||||
|
import CarouselCompetences from "./CarouselCompetences";
|
||||||
interface ImageData {
|
import { getApiUrl } from "../utils/getApiUrl";
|
||||||
url: string;
|
|
||||||
name?: string;
|
interface ImageData {
|
||||||
formats?: {
|
url: string;
|
||||||
large?: {
|
name?: string;
|
||||||
url: string;
|
formats?: {
|
||||||
};
|
large?: {
|
||||||
};
|
url: string;
|
||||||
}
|
};
|
||||||
|
};
|
||||||
interface GlossaireMot {
|
}
|
||||||
mot_clef: string;
|
|
||||||
description: string;
|
interface GlossaireMot {
|
||||||
images?: ImageData[];
|
mot_clef: string;
|
||||||
}
|
description: string;
|
||||||
|
images?: ImageData[];
|
||||||
interface ModalGlossaireProps {
|
}
|
||||||
mot: GlossaireMot;
|
|
||||||
onClose: () => void;
|
interface ModalGlossaireProps {
|
||||||
}
|
mot: GlossaireMot;
|
||||||
|
onClose: () => void;
|
||||||
export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
}
|
||||||
const apiUrl = getApiUrl();
|
|
||||||
|
export default function ModalGlossaire({ mot, onClose }: ModalGlossaireProps) {
|
||||||
useEffect(() => {
|
const apiUrl = getApiUrl();
|
||||||
document.body.classList.add("overflow-hidden");
|
|
||||||
return () => {
|
// Verrouille le scroll du body + ferme sur Esc.
|
||||||
document.body.classList.remove("overflow-hidden");
|
useEffect(() => {
|
||||||
};
|
document.body.classList.add("overflow-hidden");
|
||||||
}, []);
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") onClose();
|
||||||
const images = mot.images?.map((img) => ({
|
};
|
||||||
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
window.addEventListener("keydown", handleKey);
|
||||||
alt: img.name || "Illustration",
|
return () => {
|
||||||
})) || [];
|
document.body.classList.remove("overflow-hidden");
|
||||||
|
window.removeEventListener("keydown", handleKey);
|
||||||
return createPortal(
|
};
|
||||||
<div className="fixed inset-0 w-screen h-screen bg-black bg-opacity-75 flex items-center justify-center z-[1000]">
|
}, [onClose]);
|
||||||
<div className="bg-white/60 p-6 rounded-lg shadow-lg w-[114vw] max-w-6xl relative h-[72vh]">
|
|
||||||
<button className="absolute top-2 right-2 text-gray-700 text-sm p-1" onClick={onClose}>
|
const images =
|
||||||
✖
|
mot.images?.map((img) => ({
|
||||||
</button>
|
url: `${apiUrl}${img.formats?.large?.url || img.url}`,
|
||||||
|
alt: img.name || "Illustration",
|
||||||
<div className="flex flex-col md:flex-row gap-6 h-full">
|
})) || [];
|
||||||
{/* Description */}
|
|
||||||
<div className="md:w-1/2">
|
return createPortal(
|
||||||
<h2 className="text-3xl font-headline font-bold mb-4">{mot.mot_clef}</h2>
|
<div
|
||||||
<p className="font-headline font-bold text-xs text-gray-700 mb-6">{mot.description}</p>
|
className="fixed inset-0 z-[1000] flex items-center justify-center bg-on-surface/75 backdrop-blur-sm p-4"
|
||||||
</div>
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
<div className="md:w-1/2 h-full">
|
aria-label={`Glossaire : ${mot.mot_clef}`}
|
||||||
{images.length > 0 ? (
|
onClick={onClose}
|
||||||
<CarouselCompetences images={images} className="w-full h-full" />
|
>
|
||||||
) : (
|
{/* Carte interne : largeur fluide avec marge, hauteur capée, scroll interne si besoin.
|
||||||
<p className="text-gray-500">Aucune image disponible.</p>
|
stopPropagation pour ne pas fermer la modale quand on interagit à l'intérieur. */}
|
||||||
)}
|
<div
|
||||||
</div>
|
className="relative flex w-full max-w-4xl max-h-[90vh] flex-col overflow-hidden rounded-sheet bg-surface-container-lowest/95 backdrop-blur-vellum p-6 shadow-ambient"
|
||||||
</div>
|
onClick={(e) => e.stopPropagation()}
|
||||||
</div>
|
>
|
||||||
</div>,
|
<button
|
||||||
document.body
|
type="button"
|
||||||
);
|
className="absolute right-3 top-3 z-10 flex h-10 w-10 items-center justify-center rounded-full text-on-surface-variant transition-colors hover:bg-surface-container hover:text-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||||
}
|
onClick={onClose}
|
||||||
|
aria-label="Fermer la fenêtre du glossaire"
|
||||||
|
>
|
||||||
|
<span className="material-symbols-outlined" aria-hidden="true">
|
||||||
|
close
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex flex-col md:flex-row gap-6 overflow-y-auto pr-1">
|
||||||
|
<div className="md:w-1/2">
|
||||||
|
<h2 className="mb-4 pr-10 text-2xl font-headline font-extrabold tracking-tight text-primary md:text-3xl">
|
||||||
|
{mot.mot_clef}
|
||||||
|
</h2>
|
||||||
|
<p className="font-body text-base leading-relaxed text-on-surface-variant">
|
||||||
|
{mot.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:w-1/2 min-h-[200px] md:min-h-[320px]">
|
||||||
|
{images.length > 0 ? (
|
||||||
|
<CarouselCompetences images={images} className="h-full w-full" />
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-on-surface-variant italic">
|
||||||
|
Aucune image disponible.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -1,18 +1,47 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NavLink — lien de navigation avec état actif.
|
||||||
|
*
|
||||||
|
* Props :
|
||||||
|
* - text (string) : libellé affiché.
|
||||||
|
* - path (string) : URL cible.
|
||||||
|
* - onClick (fn, optionnel) : handler (ex. fermeture du drawer mobile).
|
||||||
|
* - className (string, optionnel) : classes utilitaires communes (fond, padding…).
|
||||||
|
* - activeClassName (string, optionnel) : classes appliquées quand la route courante == path.
|
||||||
|
* - inactiveClassName (string, optionnel) : classes appliquées quand la route courante != path.
|
||||||
|
*
|
||||||
|
* Sans activeClassName / inactiveClassName, on retombe sur le comportement
|
||||||
|
* historique `opacity-100 / opacity-50 hover:opacity-65` pour ne rien casser
|
||||||
|
* sur les NavLink du header desktop qui n'en fournissent pas.
|
||||||
|
*/
|
||||||
export default function NavLink(props) {
|
export default function NavLink(props) {
|
||||||
|
const {
|
||||||
|
text,
|
||||||
|
path,
|
||||||
|
onClick,
|
||||||
|
className = "",
|
||||||
|
activeClassName,
|
||||||
|
inactiveClassName,
|
||||||
|
} = props;
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const active = pathname === props.path;
|
const active = pathname === path;
|
||||||
|
|
||||||
|
const stateClass = active
|
||||||
|
? activeClassName ?? "opacity-100"
|
||||||
|
: inactiveClassName ?? "opacity-50 hover:opacity-65";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={active ? "opacity-100" : "opacity-50 hover:opacity-65"}
|
className={`${className} ${stateClass}`.trim()}
|
||||||
href={props.path}
|
href={path}
|
||||||
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{props.text} {/* Texte du lien */}
|
{text}
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
135
app/layout.tsx
135
app/layout.tsx
@ -1,14 +1,12 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useEffect, useState, useRef } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import Footer from "./components/Footer";
|
import Footer from "./components/Footer";
|
||||||
import "./assets/main.css";
|
import "./assets/main.css";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import NavLink from "./components/NavLink";
|
import NavLink from "./components/NavLink";
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
const [visitCount, setVisitCount] = useState(0);
|
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const menuRef = useRef<HTMLElement | null>(null);
|
const menuRef = useRef<HTMLElement | null>(null);
|
||||||
const burgerRef = useRef<HTMLButtonElement | null>(null);
|
const burgerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
@ -28,17 +26,9 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
closeTimerRef.current = setTimeout(() => setIsMenuOpen(false), AUTO_CLOSE_MS);
|
closeTimerRef.current = setTimeout(() => setIsMenuOpen(false), AUTO_CLOSE_MS);
|
||||||
};
|
};
|
||||||
|
|
||||||
const openMenu = () => setIsMenuOpen(true);
|
|
||||||
const closeMenu = () => setIsMenuOpen(false);
|
const closeMenu = () => setIsMenuOpen(false);
|
||||||
const toggleMenu = () => setIsMenuOpen((v) => !v);
|
const toggleMenu = () => setIsMenuOpen((v) => !v);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const visits = localStorage.getItem("visitCount");
|
|
||||||
const newVisitCount = visits ? parseInt(visits) + 1 : 1;
|
|
||||||
localStorage.setItem("visitCount", newVisitCount.toString());
|
|
||||||
setVisitCount(newVisitCount);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isMenuOpen) {
|
if (!isMenuOpen) {
|
||||||
clearAutoClose();
|
clearAutoClose();
|
||||||
@ -70,86 +60,146 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
}
|
}
|
||||||
}, [isMenuOpen]);
|
}, [isMenuOpen]);
|
||||||
|
|
||||||
|
// Classes communes pour les liens du drawer mobile : fond container primaire,
|
||||||
|
// radius "tile", hover qui inverse vers fond clair + texte primaire (palette Stitch).
|
||||||
|
const drawerLinkClass =
|
||||||
|
"block px-4 py-2 rounded-tile bg-primary-container/60 text-white transition-colors duration-200 hover:bg-primary-fixed hover:text-primary";
|
||||||
|
const drawerLinkActive = "bg-primary-fixed text-primary";
|
||||||
|
|
||||||
|
// NavLink desktop : état actif souligné, inactif discret (palette Stitch).
|
||||||
|
const desktopLinkActive = "text-primary border-b-2 border-primary-fixed pb-0.5";
|
||||||
|
const desktopLinkInactive =
|
||||||
|
"text-on-surface-variant hover:text-primary transition-colors";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="fr">
|
<html lang="fr">
|
||||||
<body className="min-w-0 overflow-x-hidden antialiased">
|
<body className="min-w-0 overflow-x-hidden antialiased">
|
||||||
<div className="relative grid min-h-[100dvh] w-full min-w-0 grid-rows-[auto_1fr_auto]">
|
<div className="relative grid min-h-[100dvh] w-full min-w-0 grid-rows-[auto_1fr_auto]">
|
||||||
{/* Conserve le fond en plein écran */}
|
{/* Wallpaper plein écran (fondation, ne change pas avec la refonte). */}
|
||||||
<div className="absolute inset-0 bg-wallpaper"></div>
|
<div className="absolute inset-0 bg-wallpaper"></div>
|
||||||
|
|
||||||
{/* Contenu centré avec largeur contrôlée */}
|
{/* Cercles animés : repalette en ton indigo-ardoise (Stitch "Digital Atelier"). */}
|
||||||
<div className="relative z-10 max-w-5xl w-full mx-auto"></div>
|
<div className="absolute z-0 inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div className="circle-one blur-3xl w-40 md:w-64 h-40 md:h-64 rounded-full bg-primary/40 top-0 right-10 md:right-28 absolute"></div>
|
||||||
{/* Cercles animés */}
|
<div className="circle-two blur-3xl w-40 md:w-64 h-40 md:h-64 rounded-full bg-primary-container/30 bottom-0 left-10 md:left-28 absolute"></div>
|
||||||
<div className="absolute z-0 inset-0 overflow-hidden">
|
|
||||||
<div className="circle-one blur-3xl w-40 md:w-64 h-40 md:h-64 rounded-full bg-rose-400/60 top-0 right-10 md:right-28 absolute"></div>
|
|
||||||
<div className="circle-two blur-3xl w-40 md:w-64 h-40 md:h-64 rounded-full bg-indigo-400/60 bottom-0 left-10 md:left-28 absolute"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header "No-Line" : pas de bordure pleine, juste un shift tonal + ombre ambient diffuse. */}
|
||||||
<header className="fixed left-0 top-0 z-20 h-16 w-full min-w-0 border-b-2 border-gray-500 bg-white/50 px-4 py-2 shadow-md backdrop-blur-md md:h-16 md:px-6">
|
<header className="fixed left-0 top-0 z-20 h-16 w-full min-w-0 bg-surface/80 px-4 py-2 shadow-ambient-sm backdrop-blur-vellum md:h-16 md:px-6">
|
||||||
<div className="mx-auto flex max-w-4xl min-w-0 items-center justify-between gap-2">
|
<div className="mx-auto flex max-w-4xl min-w-0 items-center justify-between gap-2">
|
||||||
<h2 className="min-w-0 truncate pr-1 text-xl font-headline font-extrabold italic tracking-tight md:text-2xl">
|
<h2 className="min-w-0 truncate pr-1 text-xl font-headline font-extrabold italic tracking-tight text-primary md:text-2xl">
|
||||||
Portfolio Gras-Calvet Fernand
|
Portfolio Gras-Calvet Fernand
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Bouton menu burger */}
|
{/* Burger ghost (Material Symbols) : plus sobre, couleur primaire, hover tonal. */}
|
||||||
<button
|
<button
|
||||||
ref={burgerRef}
|
ref={burgerRef}
|
||||||
type="button"
|
type="button"
|
||||||
className="md:hidden p-2 bg-gray-300 rounded"
|
className="md:hidden flex h-10 w-10 items-center justify-center rounded-full text-primary transition-colors hover:bg-surface-container focus:outline-none focus-visible:ring-2 focus-visible:ring-primary"
|
||||||
onClick={toggleMenu}
|
onClick={toggleMenu}
|
||||||
aria-label={isMenuOpen ? "Fermer le menu" : "Ouvrir le menu"}
|
aria-label={isMenuOpen ? "Fermer le menu" : "Ouvrir le menu"}
|
||||||
aria-expanded={isMenuOpen}
|
aria-expanded={isMenuOpen}
|
||||||
aria-controls="mobile-drawer"
|
aria-controls="mobile-drawer"
|
||||||
>
|
>
|
||||||
{isMenuOpen ? "✕" : "☰"}
|
<span className="material-symbols-outlined" aria-hidden="true">
|
||||||
|
{isMenuOpen ? "close" : "menu"}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Menu desktop */}
|
{/* Menu desktop : NavLink avec états actif/inactif éditoriaux. */}
|
||||||
<nav className="hidden md:flex">
|
<nav className="hidden md:flex">
|
||||||
<ul className="flex gap-x-4 text-gray-700 font-headline font-bold">
|
<ul className="flex gap-x-6 font-headline text-sm font-bold uppercase tracking-widest">
|
||||||
<li><NavLink text="Accueil" path="/" /></li>
|
<li>
|
||||||
<li><NavLink text="Portfolio" path="/portfolio" /></li>
|
<NavLink
|
||||||
<li><NavLink text="Compétences" path="/competences" /></li>
|
text="Accueil"
|
||||||
<li><NavLink text="Contact" path="/contact" /></li>
|
path="/"
|
||||||
|
activeClassName={desktopLinkActive}
|
||||||
|
inactiveClassName={desktopLinkInactive}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
text="Portfolio"
|
||||||
|
path="/portfolio"
|
||||||
|
activeClassName={desktopLinkActive}
|
||||||
|
inactiveClassName={desktopLinkInactive}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
text="Compétences"
|
||||||
|
path="/competences"
|
||||||
|
activeClassName={desktopLinkActive}
|
||||||
|
inactiveClassName={desktopLinkInactive}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<NavLink
|
||||||
|
text="Contact"
|
||||||
|
path="/contact"
|
||||||
|
activeClassName={desktopLinkActive}
|
||||||
|
inactiveClassName={desktopLinkInactive}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Drawer mobile (tiroir gauche, 70%, fond sombre translucide) */}
|
{/* Drawer mobile (tiroir gauche, 70 %, fond primaire translucide). */}
|
||||||
<div
|
<div
|
||||||
className={`mobile-drawer-root fixed inset-0 z-40 md:hidden transition-opacity duration-300 ease-out ${
|
className={`mobile-drawer-root fixed inset-0 z-40 md:hidden transition-opacity duration-300 ease-out ${
|
||||||
isMenuOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
isMenuOpen ? "opacity-100 pointer-events-auto" : "opacity-0 pointer-events-none"
|
||||||
}`}
|
}`}
|
||||||
aria-hidden={!isMenuOpen}
|
aria-hidden={!isMenuOpen}
|
||||||
>
|
>
|
||||||
{/* Voile : tap pour fermer */}
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
className="absolute inset-0 bg-on-surface/40 backdrop-blur-sm"
|
||||||
onClick={closeMenu}
|
onClick={closeMenu}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Colonne tiroir */}
|
|
||||||
<nav
|
<nav
|
||||||
id="mobile-drawer"
|
id="mobile-drawer"
|
||||||
ref={menuRef}
|
ref={menuRef}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Menu de navigation"
|
aria-label="Menu de navigation"
|
||||||
className={`mobile-drawer-panel relative z-10 h-full w-[70%] max-w-sm bg-gray-900/70 backdrop-blur-md border-r border-white/10 shadow-2xl flex flex-col gap-3 px-6 pt-20 pb-8 text-white font-headline font-extrabold text-2xl tracking-tight transition-transform duration-300 ease-out ${
|
className={`mobile-drawer-panel relative z-10 h-full w-[70%] max-w-sm bg-primary/90 backdrop-blur-vellum shadow-ambient flex flex-col gap-3 px-6 pt-20 pb-8 font-headline text-lg font-bold tracking-tight transition-transform duration-300 ease-out ${
|
||||||
isMenuOpen ? "translate-x-0" : "-translate-x-full"
|
isMenuOpen ? "translate-x-0" : "-translate-x-full"
|
||||||
}`}
|
}`}
|
||||||
onClick={scheduleAutoClose}
|
onClick={scheduleAutoClose}
|
||||||
onTouchStart={scheduleAutoClose}
|
onTouchStart={scheduleAutoClose}
|
||||||
onTouchMove={scheduleAutoClose}
|
onTouchMove={scheduleAutoClose}
|
||||||
>
|
>
|
||||||
<NavLink text="Accueil" path="/" onClick={closeMenu} className="text-lg text-white hover:text-gray-900 px-4 py-2 bg-gray-700/80 rounded-lg transition-all duration-300 hover:bg-gray-500" />
|
<NavLink
|
||||||
<NavLink text="Portfolio" path="/portfolio" onClick={closeMenu} className="text-lg text-white hover:text-gray-900 px-4 py-2 bg-gray-700/80 rounded-lg transition-all duration-300 hover:bg-gray-500" />
|
text="Accueil"
|
||||||
<NavLink text="Compétences" path="/competences" onClick={closeMenu} className="text-lg text-white hover:text-gray-900 px-4 py-2 bg-gray-700/80 rounded-lg transition-all duration-300 hover:bg-gray-500" />
|
path="/"
|
||||||
<NavLink text="Contact" path="/contact" onClick={closeMenu} className="text-lg text-white hover:text-gray-900 px-4 py-2 bg-gray-700/80 rounded-lg transition-all duration-300 hover:bg-gray-500" />
|
onClick={closeMenu}
|
||||||
|
className={drawerLinkClass}
|
||||||
|
activeClassName={drawerLinkActive}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
text="Portfolio"
|
||||||
|
path="/portfolio"
|
||||||
|
onClick={closeMenu}
|
||||||
|
className={drawerLinkClass}
|
||||||
|
activeClassName={drawerLinkActive}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
text="Compétences"
|
||||||
|
path="/competences"
|
||||||
|
onClick={closeMenu}
|
||||||
|
className={drawerLinkClass}
|
||||||
|
activeClassName={drawerLinkActive}
|
||||||
|
/>
|
||||||
|
<NavLink
|
||||||
|
text="Contact"
|
||||||
|
path="/contact"
|
||||||
|
onClick={closeMenu}
|
||||||
|
className={drawerLinkClass}
|
||||||
|
activeClassName={drawerLinkActive}
|
||||||
|
/>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -160,13 +210,8 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|||||||
<div className="relative z-10 w-full min-w-0 shrink-0">
|
<div className="relative z-10 w-full min-w-0 shrink-0">
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="absolute bottom-0 right-0 p-4 text-sm text-gray-500">
|
|
||||||
NV : {visitCount}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# Refonte visuelle — Direction "Digital Atelier"
|
# Refonte visuelle — Direction "Digital Atelier"
|
||||||
|
|
||||||
**Créé :** 2026-04-22
|
**Créé :** 2026-04-22
|
||||||
**Statut :** en cours (étapes 1-3/8 terminées)
|
**Statut :** en cours (étapes 1-4/8 terminées)
|
||||||
**Source d'inspiration :** `stitch_V1/` (design newsletter Stitch — `DESIGN.md` et `code.html`).
|
**Source d'inspiration :** `stitch_V1/` (design newsletter Stitch — `DESIGN.md` et `code.html`).
|
||||||
**Audit préalable :** [`captures/AUDIT-VISUEL.md`](./captures/AUDIT-VISUEL.md).
|
**Audit préalable :** [`captures/AUDIT-VISUEL.md`](./captures/AUDIT-VISUEL.md).
|
||||||
|
|
||||||
@ -57,7 +57,7 @@ Chaque étape = un lot cohérent + éventuelle mise à jour de `captures/AUDIT-V
|
|||||||
| 1 | Fondations : tokens Tailwind + import polices + icônes | `tailwind.config.ts`, `app/globals.css` | **fait** (2026-04-22) |
|
| 1 | Fondations : tokens Tailwind + import polices + icônes | `tailwind.config.ts`, `app/globals.css` | **fait** (2026-04-22) |
|
||||||
| 2 | Garde-fou doc + mise à jour feuille de route | `docs-site-interne/REFONTE-VISUELLE.md`, `docs-site-interne/feuille-de-route.md` | **fait** (2026-04-22) |
|
| 2 | Garde-fou doc + mise à jour feuille de route | `docs-site-interne/REFONTE-VISUELLE.md`, `docs-site-interne/feuille-de-route.md` | **fait** (2026-04-22) |
|
||||||
| 3 | Migration typographique globale (Orbitron → Manrope / Newsreader) | `app/**/*.{tsx,jsx,js}`, `app/assets/main.css` | **fait** (2026-04-22) |
|
| 3 | Migration typographique globale (Orbitron → Manrope / Newsreader) | `app/**/*.{tsx,jsx,js}`, `app/assets/main.css` | **fait** (2026-04-22) |
|
||||||
| 4 | Layout racine : header No-Line, burger ghost, palette cercles, compteur migré, drawer | `app/layout.tsx`, `app/globals.css` | à faire |
|
| 4 | Layout racine : header No-Line, burger ghost, palette cercles, compteur migré, drawer | `app/layout.tsx`, `app/components/NavLink.jsx`, `app/components/Footer.jsx` | **fait** (2026-04-22) |
|
||||||
| 5 | Home : hero vellum, portrait frame, takeaways, pull-quote, CTAs | `app/page.tsx` | à faire |
|
| 5 | Home : hero vellum, portrait frame, takeaways, pull-quote, CTAs | `app/page.tsx` | à faire |
|
||||||
| 6 | Listes portfolio + compétences : grille asymétrique, cartes éditoriales | `app/portfolio/page.jsx`, `app/competences/page.jsx`, composants `Carousel*` | à faire |
|
| 6 | Listes portfolio + compétences : grille asymétrique, cartes éditoriales | `app/portfolio/page.jsx`, `app/competences/page.jsx`, composants `Carousel*` | à faire |
|
||||||
| 7 | Fiches détail + modale glossaire + GrasBot (jewel flottant) | `app/portfolio/[slug]/page.tsx`, `app/competences/[slug]/page.tsx`, `app/components/ModalGlossaire.tsx`, `app/components/ChatBot.js` | à faire |
|
| 7 | Fiches détail + modale glossaire + GrasBot (jewel flottant) | `app/portfolio/[slug]/page.tsx`, `app/competences/[slug]/page.tsx`, `app/components/ModalGlossaire.tsx`, `app/components/ChatBot.js` | à faire |
|
||||||
@ -78,6 +78,30 @@ Après l'étape 3, retour utilisateur : **couleurs de texte différentes** entre
|
|||||||
|
|
||||||
**Leçon retenue (à appliquer aux étapes suivantes)** : quand on supprime un "masque" CSS (comme la couleur forcée d'Orbitron), toujours vérifier que la valeur qui va ré-émerger par héritage est bien la valeur attendue, pas une variable dépendante du contexte d'exécution.
|
**Leçon retenue (à appliquer aux étapes suivantes)** : quand on supprime un "masque" CSS (comme la couleur forcée d'Orbitron), toujours vérifier que la valeur qui va ré-émerger par héritage est bien la valeur attendue, pas une variable dépendante du contexte d'exécution.
|
||||||
|
|
||||||
|
## 4 ter. Correctif urgent modale glossaire (2026-04-22) — blocage mobile
|
||||||
|
|
||||||
|
Après l'étape 4, retour utilisateur sur Samsung S25 Ultra : les mots-clés du glossaire (compétences) ouvrent bien la modale mais **la modale déborde de l'écran**, **la croix de fermeture est hors champ**, impossible de refermer sans recharger la page.
|
||||||
|
|
||||||
|
**Causes identifiées** dans `app/components/ModalGlossaire.tsx` (pré-existantes avant la refonte) :
|
||||||
|
|
||||||
|
- Carte interne en `w-[114vw] max-w-6xl` : force une largeur > viewport sur mobile (114 % de 400 px = 456 px dans une fenêtre de 400 px), et sur desktop la contrainte est masquée par `max-w-6xl`.
|
||||||
|
- Hauteur figée `h-[72vh]` sans scroll interne : le contenu est simplement tronqué quand il dépasse.
|
||||||
|
- Aucune fermeture au tap sur le voile, ni à Esc. Seule issue = bouton `✖` en haut à droite, hors champ sur mobile.
|
||||||
|
- Bouton de fermeture en `text-sm p-1` : zone tactile < 44 px, sous le seuil Material Design pour le tactile.
|
||||||
|
|
||||||
|
**Fix** (anticipe les besoins de l'étape 7) :
|
||||||
|
|
||||||
|
- Carte interne : `w-full max-w-4xl max-h-[90vh]` + padding 4 sur le voile pour la marge latérale sur mobile.
|
||||||
|
- Contenu intérieur en `overflow-y-auto` pour scroll interne si nécessaire.
|
||||||
|
- Voile cliquable pour fermer, `stopPropagation` sur la carte pour ne pas fermer en interagissant avec.
|
||||||
|
- Fermeture `Escape` via `keydown` global.
|
||||||
|
- Bouton de fermeture rond `h-10 w-10` avec Material Symbol `close`, focus-visible, position `absolute top-3 right-3`.
|
||||||
|
- Alignement palette Stitch : voile `bg-on-surface/75 backdrop-blur-sm`, carte `bg-surface-container-lowest/95 backdrop-blur-vellum shadow-ambient rounded-sheet`, titre `text-primary`, description en `font-body` serif (Newsreader) pour lisibilité, texte `text-on-surface-variant`.
|
||||||
|
- Ajout de `"use client"` (manquant).
|
||||||
|
- `role="dialog" aria-modal="true" aria-label={...}` sur le conteneur, `aria-label` explicite sur le bouton de fermeture.
|
||||||
|
|
||||||
|
Ce correctif concerne uniquement le composant `ModalGlossaire`. L'étape 7 reprendra la refonte globale de cette zone (cohérence visuelle avec les fiches détail) mais le blocage UX mobile est levé dès maintenant.
|
||||||
|
|
||||||
## 5. Checklist relecture (à passer à la fin de chaque étape)
|
## 5. Checklist relecture (à passer à la fin de chaque étape)
|
||||||
|
|
||||||
- [ ] Aucune colonne unique globale `max-w-xl` (c'est le format newsletter).
|
- [ ] Aucune colonne unique globale `max-w-xl` (c'est le format newsletter).
|
||||||
|
|||||||
@ -8,7 +8,7 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
|
|
||||||
| ID | Sujet | Statut | Notes |
|
| ID | Sujet | Statut | Notes |
|
||||||
|----|--------|--------|--------|
|
|----|--------|--------|--------|
|
||||||
| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-3 (tokens + garde-fou + migration typo globale) faites le 2026-04-22. |
|
| R1 | Moderniser l’UI (design system, cohérence typo/couleurs) | En cours | Direction "Digital Atelier" inspirée de `stitch_V1/` ; cadrage et plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md). Étapes 1-4 (tokens + garde-fou + migration typo globale + layout racine) faites le 2026-04-22. |
|
||||||
| R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` |
|
| R2 | Homogénéiser TS vs JS dans `app/` | À faire | Migrer progressivement les `.jsx`/`.js` |
|
||||||
| R3 | Centraliser config API (Strapi + LLM) via `.env` | À faire | Remplacer URLs en dur où pertinent |
|
| R3 | Centraliser config API (Strapi + LLM) via `.env` | À faire | Remplacer URLs en dur où pertinent |
|
||||||
| R4 | Revoir `layout.tsx` (server vs client, perf SEO) | À faire | Évaluer extraction header/footer |
|
| R4 | Revoir `layout.tsx` (server vs client, perf SEO) | À faire | Évaluer extraction header/footer |
|
||||||
@ -40,3 +40,5 @@ Document vivant : ajuster les statuts et dates au fil du travail.
|
|||||||
| 2026-04-22 | Refonte visuelle "Digital Atelier" — étape 1 (tokens Tailwind : palette Stitch, `font-headline` Manrope + `font-body` Newsreader, `rounded-sheet/tile`, `shadow-ambient/jewel`) et étape 2 (garde-fou + plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md)). |
|
| 2026-04-22 | Refonte visuelle "Digital Atelier" — étape 1 (tokens Tailwind : palette Stitch, `font-headline` Manrope + `font-body` Newsreader, `rounded-sheet/tile`, `shadow-ambient/jewel`) et étape 2 (garde-fou + plan dans [`REFONTE-VISUELLE.md`](./REFONTE-VISUELLE.md)). |
|
||||||
| 2026-04-22 | Refonte visuelle — étape 3 : migration typographique globale. Toutes les classes `font-orbitron-*` (12 définitions CSS, 29 occurrences dans 11 fichiers) remplacées par `font-headline` Manrope + tailles/poids Tailwind explicites. Import Google Fonts Orbitron supprimé de `app/assets/main.css`. |
|
| 2026-04-22 | Refonte visuelle — étape 3 : migration typographique globale. Toutes les classes `font-orbitron-*` (12 définitions CSS, 29 occurrences dans 11 fichiers) remplacées par `font-headline` Manrope + tailles/poids Tailwind explicites. Import Google Fonts Orbitron supprimé de `app/assets/main.css`. |
|
||||||
| 2026-04-22 | Refonte visuelle — correctif post-étape 3 : régression de couleurs texte entre desktop/mobile. Retrait du `@media (prefers-color-scheme: dark)` hérité du template Next (incohérent avec l'arbitrage "light-only"), `--foreground` fixé à `#191c1d` (on-surface Stitch), `body` avec couleur non-dépendante du thème système. 3 classes Tailwind invalides `text-black-500/700` remplacées par `text-gray-700` (`app/layout.tsx`, `app/page.tsx`, `app/components/ContentSectionCompetences.tsx`). |
|
| 2026-04-22 | Refonte visuelle — correctif post-étape 3 : régression de couleurs texte entre desktop/mobile. Retrait du `@media (prefers-color-scheme: dark)` hérité du template Next (incohérent avec l'arbitrage "light-only"), `--foreground` fixé à `#191c1d` (on-surface Stitch), `body` avec couleur non-dépendante du thème système. 3 classes Tailwind invalides `text-black-500/700` remplacées par `text-gray-700` (`app/layout.tsx`, `app/page.tsx`, `app/components/ContentSectionCompetences.tsx`). |
|
||||||
|
| 2026-04-22 | Refonte visuelle — étape 4 : layout racine. Header "No-Line" (bordure pleine supprimée, `shadow-ambient-sm` + `backdrop-blur-vellum`, titre en `text-primary`). Burger refait en ghost button (Material Symbols `menu`/`close` au lieu des caractères `☰`/`✕`). Cercles animés repeints en `bg-primary/40` + `bg-primary-container/30`. Drawer mobile en `bg-primary/90 backdrop-blur-vellum` + liens éditoriaux (`bg-primary-container/60` → hover `bg-primary-fixed text-primary`). Bug préexistant **corrigé** : `NavLink` ignorait `className` et `onClick` fournis par le drawer mobile → refait avec support `className` / `onClick` / `activeClassName` / `inactiveClassName`, comportement desktop historique préservé. Compteur de visites migré de `layout.tsx` (bloc orphelin `absolute bottom-0 right-0`) vers `Footer.jsx` (ligne discrète `text-[10px] uppercase tracking-[0.3em]`). Nettoyage : state `visitCount` + useEffect déplacés, `div.max-w-5xl` vide retirée, state `count` inutilisé retiré de `Footer.jsx`. |
|
||||||
|
| 2026-04-22 | Refonte visuelle — correctif urgent `ModalGlossaire` (blocage mobile signalé sur Samsung S25 Ultra). `w-[114vw] max-w-6xl h-[72vh]` → `w-full max-w-4xl max-h-[90vh]` + `overflow-y-auto`. Fermeture ajoutée sur tap-voile et `Escape`. Bouton fermeture rond 40 px Material Symbol `close`. Alignement palette Stitch (`bg-on-surface/75`, `bg-surface-container-lowest/95`, `rounded-sheet`, `text-primary`, description `font-body` Newsreader). `"use client"` + `role="dialog" aria-modal` ajoutés. |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user