Page d’exemple (/exemples/…) — le verdict de Pathou sur une trace de référence, tel que le moteur l’a rendu ; aucun chiffre n’est écrit à la main.
stableÀ valider
Écran ou section propre à Pathou, pas un composant du design system : à ne pas réutiliser ailleurs. Tous les écrans et sections
// Titre : Verdict d’exemple : Davos en VTT
import ExemplePage from '@pathou/components/ExemplePage';
export default function Exemple() {
return (
<div className="ip-main" style={{ width: 760, maxHeight: 680, overflowY: 'auto' }}>
<ExemplePage slug="davos-vtt" />
</div>
);
}
Installation
Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.
import { KNOWLEDGE_LABEL } from '../lib/difficulty/confidence';
import type { VerdictLevel } from '../lib/difficulty/model';
import { LEVELS, LEVEL_LABEL, PRACTICE_LABEL } from '../lib/difficulty/rules';
import { fmt } from '../lib/format';
import { WAY_CLASS_LABEL } from '../lib/osm/classify';
import { formatHours } from '../lib/time';
import type { Level, Practice } from '../lib/types';
import { EN_PRATIQUE, EXEMPLES, type ExempleData, type ExempleRider, type ExempleSlug } from '../pages/exemples';
import VERDICTS from '../pages/exemples-verdicts.json';
const cap = (s: string) => s[0].toUpperCase() + s.slice(1);
const DATA = VERDICTS as Record<ExempleSlug, ExempleData>;
const PRACTICES: Practice[] = ['vtt', 'gravel', 'route'];
const VERDICT_LABEL: Record<VerdictLevel, string> = { ok: 'Adaptée', engage: 'Engagée', exigeant: 'Exigeante', inadapte: 'Non adaptée' };
const label = (r: ExempleRider) => (r.volumeOnly ? (r.verdict === 'exigeant' ? 'Très longue' : 'Longue') : VERDICT_LABEL[r.verdict]);
const CONFIANCE: Record<ExempleRider['confianceNiveau'], string> = { elevee: 'élevée', moyenne: 'moyenne', faible: 'faible' };
const MOIS = ['janvier', 'février', 'mars', 'avril', 'mai', 'juin', 'juillet', 'août', 'septembre', 'octobre', 'novembre', 'décembre'];
const date = (iso: string) => {
const [y, m, d] = iso.split('-').map(Number);
return `${d === 1 ? '1er' : d} ${MOIS[m - 1]} ${y}`;
};
const km = (v: number) => `${fmt(v, v < 10 ? 1 : 0)} km`;
const longueur = (m: number) => (m >= 1000 ? `${fmt(m / 1000, 1)} km` : `${fmt(m)} m`);
/** Confiance d'une pratique : une valeur, ou l'écart entre niveaux s'ils diffèrent. */
function confiances(data: ExempleData, p: Practice) {
const v = LEVELS.map((l) => data.riders[`${p}/${l}`].confiance);
const lo = Math.min(...v);
const hi = Math.max(...v);
return lo === hi ? `${lo} %` : `${lo} à ${hi} %`;
}
/** Lien qui ouvre la trace dans l'outil, réglé sur la pratique et le niveau (App.tsx). */
export const analyseHref = (slug: ExempleSlug, p: Practice, l: Level) => `/?exemple=${slug}&pratique=${p}&niveau=${l}`;
/**
* Page d'exemple : le verdict de Pathou sur une trace de référence, tel que le moteur l'a rendu (exemples-verdicts.json,
* `pnpm reference:exemples`). Aucun chiffre n'est écrit à la main : tout vient du fichier figé.
*/
export default function ExemplePage({ slug }: { slug: ExempleSlug }) {
const ex = EXEMPLES.find((e) => e.slug === slug)!;
const data = DATA[slug];
const p: Practice = ex.pratique;
const rider = (l: Level) => data.riders[`${p}/${l}`];
const ref = rider('intermediaire');
// points d'attention de la pratique de la page, regroupés par nom : nombre et longueur cumulée par niveau
const noms = [...new Set(LEVELS.flatMap((l) => rider(l).alertes.map((a) => a.nom)))];
const groupe = (l: Level, nom: string) => {
const list = rider(l).alertes.filter((a) => a.nom === nom);
return { n: list.length, m: list.reduce((s, a) => s + a.longueurM, 0), obstacle: list.every((a) => a.type === 'obstacle') };
};
const longs = [...ref.alertes].filter((a) => a.type !== 'obstacle').sort((a, b) => b.longueurM - a.longueurM).slice(0, 5).sort((a, b) => a.km - b.km);
const autres = EXEMPLES.filter((e) => e.slug !== slug);
return (
<div className="ip-prose ex-page">
<p className="modal-lead">{ex.intro}</p>
<p>
Voici le verdict que Pathou rend sur cette trace, pour chaque pratique et chaque niveau. Rien n’est estimé à la main : chaque chiffre de la page
sort du moteur d’analyse, qui a analysé la trace le {date(data.analyse)}.
</p>
<dl className="ex-facts">
<div>
<dt>Distance</dt>
<dd>{km(data.distanceKm)}</dd>
</div>
<div>
<dt>Dénivelé</dt>
<dd>
+{fmt(data.deniveleM)} m · −{fmt(data.descenteM)} m
</dd>
</div>
<div>
<dt>
{cap(EN_PRATIQUE[p])}, niveau {LEVEL_LABEL.intermediaire.toLowerCase()}
</dt>
<dd>
<span className={`dock-badge badge-${ref.verdict}`}>{label(ref)}</span>
</dd>
</div>
<div>
<dt>Confiance du verdict</dt>
<dd>
{CONFIANCE[ref.confianceNiveau]} · {ref.confiance} %
</dd>
</div>
</dl>
<section aria-labelledby="ex-niveaux">
<h2 id="ex-niveaux">{`Le verdict ${EN_PRATIQUE[p]}, niveau par niveau`}</h2>
<ul className="ex-levels">
{LEVELS.map((l) => {
const r = rider(l);
return (
<li key={l}>
<b>{LEVEL_LABEL[l]}</b>
<span className={`dock-badge badge-${r.verdict}`}>{label(r)}</span>
<span>
{r.titre}.{!r.titre.includes('en roulant') && ` Environ ${formatHours(r.heures)} en roulant.`}{' '}
<a className="hp-link" href={analyseHref(slug, p, l)}>
Voir l’analyse
</a>
</span>
</li>
);
})}
</ul>
</section>
{noms.length > 0 && (
<section aria-labelledby="ex-attention">
<h2 id="ex-attention">Les principaux points d’attention</h2>
<p>
Les passages que Pathou signale {EN_PRATIQUE[p]}, selon le niveau : un passage trop raide ou trop technique pour un débutant peut se
rouler pour un confirmé.
</p>
<div className="g-table">
<table>
<thead>
<tr>
<th scope="col">Point d’attention</th>
{LEVELS.map((l) => (
<th scope="col" key={l}>
{LEVEL_LABEL[l]}
</th>
))}
</tr>
</thead>
<tbody>
{noms.map((nom) => (
<tr key={nom}>
<th scope="row">{nom}</th>
{LEVELS.map((l) => {
const g = groupe(l, nom);
return <td key={l}>{g.n === 0 ? '–' : g.obstacle ? `${g.n}` : `${g.n} · ${longueur(g.m)}`}</td>;
})}
</tr>
))}
</tbody>
</table>
</div>
<p className="muted small">Nombre de passages et longueur cumulée.</p>
{longs.length > 0 && (
<>
<h3 className="ex-sub">Les plus longs, pour un niveau {LEVEL_LABEL.intermediaire.toLowerCase()}</h3>
<ul className="ex-list">
{longs.map((a) => (
<li key={`${a.km}-${a.titre}`}>
Au km {fmt(a.km, 1)} : {a.titre}
</li>
))}
</ul>
</>
)}
</section>
)}
<section aria-labelledby="ex-pratiques">
<h2 id="ex-pratiques">En VTT, en gravel et sur route</h2>
<div className="g-table">
<table>
<thead>
<tr>
<th scope="col">Pratique</th>
{LEVELS.map((l) => (
<th scope="col" key={l}>
{LEVEL_LABEL[l]}
</th>
))}
<th scope="col">Confiance</th>
</tr>
</thead>
<tbody>
{PRACTICES.map((q) => (
<tr key={q}>
<th scope="row">{PRACTICE_LABEL[q]}</th>
{LEVELS.map((l) => {
const r = data.riders[`${q}/${l}`];
return (
<td key={l}>
<a className={`dock-badge badge-${r.verdict}`} href={analyseHref(slug, q, l)} title={r.titre}>
{label(r)}
</a>
</td>
);
})}
<td>{confiances(data, q)}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
<section aria-labelledby="ex-donnees">
<h2 id="ex-donnees">Sur quoi repose ce verdict</h2>
<p>
La confiance dit à quel point le verdict s’appuie sur des données précises, pas la difficulté. Elle est {CONFIANCE[ref.confianceNiveau]} ici (
{ref.confiance} % {EN_PRATIQUE[p]}, niveau {LEVEL_LABEL.intermediaire.toLowerCase()}).
</p>
<div className="ex-cols">
<div>
<h3 className="ex-sub">Ce que l’on sait des chemins</h3>
<ul className="ex-list">
{ref.facteurs.map((f) => (
<li key={f.type}>
{KNOWLEDGE_LABEL[f.type]} : {km(f.km)}
</li>
))}
</ul>
</div>
<div>
<h3 className="ex-sub">Nature des voies</h3>
<ul className="ex-list">
{data.voies.map((v) => (
<li key={v.type}>
{WAY_CLASS_LABEL[v.type]} : {km(v.km)}
</li>
))}
</ul>
</div>
</div>
<p>
Altitude :{' '}
{data.altitudes.map((a) => `${a.source} (maille de ${fmt(a.resolutionM)} m) sur ${km(a.km)}`).join(', ')}. Chemins : OpenStreetMap.
</p>
</section>
<section aria-labelledby="ex-soi">
<h2 id="ex-soi">Analyser cette trace toi-même</h2>
<p>
Ouvre la trace dans Pathou pour voir son relief en 3D, son profil et chaque passage sur la carte, avec les contournements proposés. Les chemins
d’OpenStreetMap changent : l’analyse du jour peut s’écarter un peu de celle de cette page.
</p>
<p className="ex-actions">
<a className="ip-start" href={analyseHref(slug, p, 'intermediaire')}>
Analyser cette trace dans Pathou
</a>
<a className="hp-link" href={`/exemples/${slug}.gpx`} download>
Télécharger la trace GPX
</a>
</p>
<p className="muted small">
Trace construite par Pathou sur les voies d’OpenStreetMap : données ©
<a className="hp-link" href="https://www.openstreetmap.org/copyright">
contributeurs d’OpenStreetMap
</a>
, disponibles sous la{' '}
<a className="hp-link" href="https://opendatacommons.org/licenses/odbl/1-0/">
licence Open Database (ODbL)
</a>
.
</p>
</section>
<section aria-labelledby="ex-limites">
<h2 id="ex-limites">Ce que le verdict ne dit pas</h2>
<p>
La boue, la météo, la végétation et ta forme du jour ne se lisent pas dans un fichier GPX. Le verdict est indicatif : avant de partir, vérifie
les passages signalés sur la photo aérienne et sur les photos au sol.
</p>
</section>
<section aria-labelledby="ex-autres">
<h2 id="ex-autres">D’autres traces analysées</h2>
<ul className="ex-list">
{autres.map((e) => (
<li key={e.slug}>
<a className="hp-link strong" href={`/exemples/${e.slug}`}>
{e.lieu}, {EN_PRATIQUE[e.pratique]}
</a>
</li>
))}
<li>
<a className="hp-link" href="/trace-gpx-faisable">
Les repères pour juger une trace soi-même
</a>
</li>
</ul>
</section>
</div>
);
}
Tokens : --panel --text --muted --line --accent --accent-ink --ok (non déterminé) --engage (non déterminé) --exigeant (non déterminé) --inadapte (non déterminé)
Utilisation
import ExemplePage from './components/ExemplePage';
<ExemplePage slug="davos-vtt" />Guidelines
- MUSTNe montrer que des chiffres tirés du fichier figé par le moteur (exemples-verdicts.json,
pnpm reference:exemples). - SHOULDProposer d’ouvrir la trace dans l’outil, réglée sur la pratique et le niveau de l’exemple.
Référence de l’API
| Prop | Type | Défaut |
|---|---|---|
slug | ExempleSlug | requis |