HomeDemo

←→

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.

src/components/HomeDemo.tsx
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

  • MUST
    Coter la démonstration avec le vrai moteur (rateDemo), jamais avec des couleurs écrites à la main.
  • SHOULD
    Suivre en direct les réglages de l’accueil, pour montrer l’effet du niveau avant tout fichier.

Référence de l’API

PropTypeDéfaut
practicePracticerequis
levelLevelrequis