Démonstration de l’accueil — un même parcours dont le profil se recolore et le verdict change avec la pratique et le niveau choisis, cotés par le vrai moteur.
stableÀ valider
// Titre : Démonstration, VTT intermédiaire
import HomeDemo from '@pathou/components/HomeDemo';
export default function Exemple() {
return (
<div style={{ width: 520 }}>
<HomeDemo practice="vtt" level="intermediaire" />
</div>
);
}
Installation
Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.
import { useMemo } from 'react';
import { RATING_COLOR } from '../lib/colors';
import { DEMO_PROFILE, DEMO_STATS, rateDemo } from '../lib/demoTrack';
import { RATINGS, RATING_LABEL } from '../lib/difficulty/rules';
import { fmt } from '../lib/format';
import type { Verdict } from '../lib/difficulty/model';
import type { Level, Practice, Rating } from '../lib/types';
const VERDICT_LABEL: Record<Verdict['level'], string> = { ok: 'Adaptée', engage: 'Engagée', exigeant: 'Exigeante', inadapte: 'Non adaptée' };
/** Libellé court ; une trace facile mais longue est dite longue, pas engagée. */
const verdictLabel = (v: Verdict) => (v.volumeOnly ? (v.level === 'exigeant' ? 'Très longue' : 'Longue') : VERDICT_LABEL[v.level]);
const W = 520;
const H = 178;
const TOP = 26;
const BOTTOM = 22;
const AXIS_KM = [0, 10, 20, 30];
const round10 = (n: number) => Math.round(n / 10) * 10;
/** Le noir de « très difficile » disparaît sur fond sombre : il suit la couleur du texte. */
const color = (r: Rating) => (r === 'noir' ? 'var(--text)' : RATING_COLOR[r]);
/** « Exigeante : 8,8 km très difficiles… » → « 8,8 km très difficiles… » (le badge dit déjà le niveau). */
function sentence(title: string): string {
const s = title.includes('\u00a0: ') ? title.slice(title.indexOf('\u00a0: ') + 3) : title;
return `${s.charAt(0).toUpperCase()}${s.slice(1)}.`;
}
/**
* Exemple de parcours de l'accueil : le profil se recolore et le verdict change avec la pratique et le niveau choisis,
* cotés par le vrai moteur. C'est la démonstration du produit avant même de charger un fichier.
*/
export default function HomeDemo({ practice, level }: { practice: Practice; level: Level }) {
const demo = useMemo(() => rateDemo(practice, level), [practice, level]);
const { ele } = DEMO_PROFILE;
const n = demo.ratings.length;
const eMin = Math.min(...ele) - 40;
const eMax = Math.max(...ele) + 30;
const x = (i: number) => (i / n) * W;
const y = (v: number) => H - BOTTOM - ((v - eMin) / (eMax - eMin)) * (H - BOTTOM - TOP);
return (
<section className="hp-demo" aria-label="Exemple de parcours">
<div className="hp-demo-head">
<b>Le même parcours, selon ton niveau</b>
<span>
Exemple de {Math.round(DEMO_STATS.km)} km et {fmt(round10(DEMO_STATS.gain))} m de dénivelé positif
</span>
</div>
<svg className="hp-prof" viewBox={`0 0 ${W} ${H}`} role="img" aria-label={`Profil d’altitude d’un parcours d’exemple, coloré selon la difficulté pour ${practice === 'vtt' ? 'le VTT' : practice === 'gravel' ? 'le gravel' : 'la route'}`}>
<defs>
<clipPath id="hp-clip">
<rect className="hp-reveal" x="0" y="0" width={W} height={H} />
</clipPath>
</defs>
<g clipPath="url(#hp-clip)">
{[0, 1, 2, 3].map((k) => (
<line key={k} className="hp-grid" x1="0" x2={W} y1={TOP + (k * (H - BOTTOM - TOP)) / 3} y2={TOP + (k * (H - BOTTOM - TOP)) / 3} />
))}
{demo.ratings.map((r, i) => (
<path key={`f${i}`} d={`M${x(i)} ${y(ele[i])}L${x(i + 1)} ${y(ele[i + 1])}L${x(i + 1)} ${H - BOTTOM}L${x(i)} ${H - BOTTOM}Z`} fill={color(r)} fillOpacity="0.28" />
))}
{demo.ratings.map((r, i) => (
<polyline key={`l${i}`} points={`${x(i)},${y(ele[i])} ${x(i + 1)},${y(ele[i + 1])}`} fill="none" stroke={color(r)} strokeWidth="3.2" strokeLinecap="round" />
))}
{demo.hard.map((h) => {
const cx = (x(h.from) + x(h.to + 1)) / 2;
return <path key={h.from} d={`M${cx - 5} 5L${cx + 5} 5L${cx} 15Z`} fill={h.rating === 'impraticable' ? RATING_COLOR.impraticable : RATING_COLOR.rouge} />;
})}
<line className="hp-base" x1="0" x2={W} y1={H - BOTTOM} y2={H - BOTTOM} />
</g>
{AXIS_KM.map((km) => (
<text key={km} className="hp-axis" x={(km / DEMO_STATS.km) * W + (km === 0 ? 2 : 0)} y={H - 6} textAnchor={km === 0 ? 'start' : 'middle'}>
{km === 0 ? '0' : `${km} km`}
</text>
))}
</svg>
<ul className="hp-legend">
{RATINGS.map((r) => (
<li key={r}>
<i style={{ background: color(r) }} />
{RATING_LABEL[r]}
</li>
))}
</ul>
<div className="hp-verdict" aria-live="polite">
<span className={`hp-badge badge-${demo.verdict.level}`}>{verdictLabel(demo.verdict)}</span>
<span>{sentence(demo.verdict.title)}</span>
</div>
</section>
);
}
Tokens : --panel --text --muted --line --ok (non déterminé) --engage (non déterminé) --exigeant (non déterminé) --inadapte (non déterminé) --ease-out
Utilisation
import HomeDemo from './components/HomeDemo';
<HomeDemo practice={practice} level={level} />Exemples
La même trace, route débutant
// Titre : La même trace, route débutant
import HomeDemo from '@pathou/components/HomeDemo';
export default function Exemple() {
return (
<div style={{ width: 520 }}>
<HomeDemo practice="route" level="debutant" />
</div>
);
}
Guidelines
- MUSTCoter la démonstration avec le vrai moteur (rateDemo), jamais avec des couleurs écrites à la main.
- SHOULDSuivre en direct les réglages de l’accueil, pour montrer l’effet du niveau avant tout fichier.
Référence de l’API
| Prop | Type | Défaut |
|---|---|---|
practice | Practice | requis |
level | Level | requis |