ExemplePage

←→

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.

src/components/ExemplePage.tsx
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&nbsp;: 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)}&nbsp;m · −{fmt(data.descenteM)}&nbsp;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}&nbsp;%
          </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&nbsp;: 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)}&nbsp;: {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}&nbsp;% {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]}&nbsp;: {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]}&nbsp;: {km(v.km)}
                </li>
              ))}
            </ul>
          </div>
        </div>
        <p>
          Altitude&nbsp;:{' '}
          {data.altitudes.map((a) => `${a.source} (maille de ${fmt(a.resolutionM)} m) sur ${km(a.km)}`).join(', ')}. Chemins&nbsp;: 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&nbsp;: 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&nbsp;: données ©&nbsp;
          <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&nbsp;: 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

  • MUST
    Ne montrer que des chiffres tirés du fichier figé par le moteur (exemples-verdicts.json, pnpm reference:exemples).
  • SHOULD
    Proposer 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

PropTypeDéfaut
slugExempleSlugrequis