ShareDialog

←→

Fenêtre de partage de la trace par un lien qui contient la trace elle-même ; rien n’est enregistré chez Pathou.

stableÀ valider
// Titre : Lien prêt à copier
import { useState } from 'react';
import ShareDialog from '@pathou/components/ShareDialog';

// dans Pathou, makeLink encode la trace dans le lien (lib/share.ts) ; ici, un lien d’exemple
const makeLink = async () => 'https://pathou.com/#t=exemple';

export default function Exemple() {
  const [open, setOpen] = useState(true);
  return (
    <div style={{ minHeight: 380 }}>
      <button type="button" onClick={() => setOpen(true)}>
        Partager
      </button>
      {open && <ShareDialog practice="vtt" level="confirme" applied={1} makeLink={makeLink} onClose={() => setOpen(false)} />}
    </div>
  );
}

Installation

Copie le fichier dans ton projet, ainsi que les tokens qu’il utilise.

src/components/ShareDialog.tsx
import { useEffect, useRef, useState } from 'react';
import { track } from '../lib/analytics';
import { LEVEL_LABEL } from '../lib/difficulty/rules';
import type { Level, Practice } from '../lib/types';
import Modal from './Modal';

const PRACTICE_TEXT: Record<Practice, string> = { vtt: 'VTT', gravel: 'gravel', route: 'vélo de route' };

/**
 * Partage de la trace affichée par un lien qui la contient (lib/share.ts) : rien n'est enregistré chez Pathou.
 * Le lien se copie.
 */
export default function ShareDialog({
  practice,
  level,
  applied,
  makeLink,
  onClose,
}: {
  practice: Practice;
  level: Level;
  /** alternatives appliquées, incluses dans la trace partagée */
  applied: number;
  makeLink: () => Promise<string>;
  onClose: () => void;
}) {
  const [link, setLink] = useState<string | null>(null);
  const [failed, setFailed] = useState(false);
  const [copied, setCopied] = useState<'ok' | 'ko' | null>(null);
  const field = useRef<HTMLInputElement>(null);

  useEffect(() => {
    let live = true;
    makeLink().then(
      (l) => live && setLink(l),
      () => live && setFailed(true),
    );
    return () => {
      live = false;
    };
  }, [makeLink]);

  const copy = async () => {
    if (!link) return;
    try {
      await navigator.clipboard.writeText(link);
      setCopied('ok');
      track('lien-partage', { moyen: 'copie' });
    } catch {
      // presse-papiers refusé (navigateur, permission) : le lien est sélectionné, à copier à la main
      field.current?.select();
      setCopied('ko');
    }
  };

  return (
    <Modal title="Partager cette trace" onClose={onClose} small>
      <p className="modal-lead">
        Le lien contient la trace elle-même{applied > 0 ? ', alternatives appliquées comprises' : ''}. La personne qui l’ouvre voit le parcours et le verdict en{' '}
        {PRACTICE_TEXT[practice]}, niveau {LEVEL_LABEL[level].toLowerCase()}, recalculés dans son navigateur. Rien n’est enregistré chez Pathou.
      </p>
      <p className="modal-lead share-note">Toute personne qui a le lien peut voir le parcours, départ et arrivée compris.</p>
      {failed ? (
        <p className="error">Le lien n’a pas pu être préparé. Télécharge le GPX pour partager la trace.</p>
      ) : (
        <input
          ref={field}
          className="share-link"
          readOnly
          value={link ?? 'Préparation du lien…'}
          aria-label="Lien de partage"
          onFocus={(e) => e.target.select()}
        />
      )}
      <div className="modal-actions">
        {copied === 'ko' && <span className="share-copied">Copie impossible ici&nbsp;: le lien est sélectionné, copie-le à la main.</span>}
        <button type="button" className="primary" onClick={copy} disabled={!link}>
          {copied === 'ok' ? 'Lien copié ✓' : 'Copier le lien'}
        </button>
      </div>
    </Modal>
  );
}

Tokens : --bg --panel --text --muted --line --accent --accent-ink --chip --exigeant --ease-out

Utilisation

import ShareDialog from './components/ShareDialog';

{sharing && <ShareDialog practice={practice} level={level} applied={applied.length} makeLink={makeShareLink} onClose={() => setSharing(false)} />}

Exemples

Lien impossible à préparer

// Titre : Lien impossible à préparer
import { useState } from 'react';
import ShareDialog from '@pathou/components/ShareDialog';

const makeLink = () => Promise.reject(new Error('trace trop longue'));

export default function Exemple() {
  const [open, setOpen] = useState(true);
  return (
    <div style={{ minHeight: 340 }}>
      <button type="button" onClick={() => setOpen(true)}>
        Partager
      </button>
      {open && <ShareDialog practice="gravel" level="intermediaire" applied={0} makeLink={makeLink} onClose={() => setOpen(false)} />}
    </div>
  );
}

Anatomie

#ÉlémentRôle
1ExplicationCe que contient le lien et ce que verra la personne qui l’ouvre (pratique, niveau, alternatives comprises).
2AvertissementLe lien montre le parcours, départ et arrivée compris, à toute personne qui l’a.
3LienChamp en lecture seule, nommé « Lien de partage », sélectionné au focus.
4CopierBouton principal ; « Lien copié ✓ » une fois fait, ou le lien sélectionné si le presse-papiers est refusé.

États

linkÉtat du lien, préparé à l’ouverture par makeLink.

Guidelines

  • MUST
    Prévenir que le lien révèle le départ et l’arrivée ; une trace part souvent de chez soi.
  • MUST
    Prévoir l’échec de la copie (navigateur, permission) en sélectionnant le lien pour une copie à la main.
  • SHOULD
    Proposer le téléchargement du GPX quand le lien ne peut pas être préparé.

Référence de l’API

PropTypeDéfaut
practicePracticerequis
levelLevelrequis
applied
alternatives appliquées, incluses dans la trace partagée
numberrequis
makeLink() => Promise<string>requis
onClose() => voidrequis