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.
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 : 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ément | Rôle |
|---|---|---|
| 1 | Explication | Ce que contient le lien et ce que verra la personne qui l’ouvre (pratique, niveau, alternatives comprises). |
| 2 | Avertissement | Le lien montre le parcours, départ et arrivée compris, à toute personne qui l’a. |
| 3 | Lien | Champ en lecture seule, nommé « Lien de partage », sélectionné au focus. |
| 4 | Copier | Bouton 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
- MUSTPrévenir que le lien révèle le départ et l’arrivée ; une trace part souvent de chez soi.
- MUSTPrévoir l’échec de la copie (navigateur, permission) en sélectionnant le lien pour une copie à la main.
- SHOULDProposer le téléchargement du GPX quand le lien ne peut pas être préparé.
Référence de l’API
| Prop | Type | Défaut |
|---|---|---|
practice | Practice | requis |
level | Level | requis |
appliedalternatives appliquées, incluses dans la trace partagée | number | requis |
makeLink | () => Promise<string> | requis |
onClose | () => void | requis |