diff --git a/.nuxtrc b/.nuxtrc index 640f280..0e6b68e 100644 --- a/.nuxtrc +++ b/.nuxtrc @@ -1 +1 @@ -setups.@nuxt/test-utils="4.0.3" \ No newline at end of file +setups.@nuxt/test-utils="4.1.0" \ No newline at end of file diff --git a/README.md b/README.md index 85ffdd4..ff4d3f9 100644 --- a/README.md +++ b/README.md @@ -100,9 +100,13 @@ condition de publier vos modifications. Un abonnement SNCF pour les 16-27 ans qui donne accès à un nombre illimité de trajets sur les trains éligibles, dans la limite des places réservées à l'abonnement. Ces places sont -contingentées : un train peut circuler sans être ouvert à TGVmax. C'est ce contingent que +contingentées : un train peut circuler sans être ouvert à l'abonnement. C'est ce contingent que Trainquillou rend visible. +La SNCF a renommé l'offre **MAX JEUNE** en 2023. « TGVmax » reste le nom sous lequel la plupart +des abonnés la connaissent, et celui du dataset open data : les deux termes cohabitent donc dans +l'interface et dans les métadonnées, sans en privilégier un. + ### Trainquillou réserve-t-il mes billets ? Non. Il montre où il reste des places et renvoie vers SNCF Connect ou Trainline pour la @@ -128,9 +132,12 @@ service gratuit, sans but lucratif. ### Mes visites sont-elles suivies ? -L'instance officielle mesure son audience avec [Rybbit](https://rybbit.io) : sans cookie, sans -identifiant persistant, sans profil publicitaire, données hébergées dans l'UE. Aucun bandeau de -consentement n'est nécessaire, faute de donnée personnelle collectée. +L'instance officielle mesure son audience avec [Rybbit](https://rybbit.com) : sans cookie, sans +identifiant persistant, sans profil publicitaire, et sans stockage des adresses IP d'après sa +[politique de confidentialité](https://rybbit.com/privacy). Aucun bandeau de consentement n'est +nécessaire, faute de donnée personnelle collectée. + +Le site lui-même tourne sur un VPS OVH à Gravelines (Nord, France), sans CDN intermédiaire. Rien de tout cela n'est actif dans le code que vous clonez : la mesure ne s'active que si vous fournissez votre propre identifiant au build (voir ci-dessous). Une instance auto-hébergée @@ -196,6 +203,26 @@ uv run scripts/build-booking.py # slugs de ville pour les liens de réserva Ils nécessitent [uv](https://docs.astral.sh/uv/) ; les dépendances sont déclarées dans l'en-tête de chaque script. +### Images du hero + +L'image d'accueil est l'élément LCP de la page : ses variantes sont générées à la main et +commitées, plutôt que produites par un module Nuxt. L'image ne change jamais, et `.output/` +doit rester sans binaire natif pour qu'un build lancé depuis un Mac arm64 tourne dans le +conteneur amd64 (voir `infra/deploy.sh`). Deux variantes AVIF (69 % de moins que le JPEG à +qualité indiscernable), `hero.jpg` en repli pour les navigateurs sans AVIF. + +Pour les régénérer, avec [vips](https://www.libvips.org/) (`brew install vips`) : + +```bash +vips thumbnail public/hero.jpg 'public/hero-800.avif[Q=58]' 800 +vips thumbnail public/hero.jpg 'public/hero-1672.avif[Q=58]' 1672 +vips thumbnail public/hero.jpg public/hero-800.jpg 800 +vips thumbnail public/hero.jpg public/hero-1200.jpg 1200 +``` + +**Ne pas utiliser `sips` pour l'AVIF** : il produit un fichier dont Chromium lit les dimensions +mais pas les pixels, et le hero s'affiche vide. + ## Contribuer Les contributions sont bienvenues. Quelques conventions : diff --git a/app/components/DestinationCard.vue b/app/components/DestinationCard.vue index 7324e5f..77eaf14 100644 --- a/app/components/DestinationCard.vue +++ b/app/components/DestinationCard.vue @@ -2,14 +2,8 @@ import type { Destination, SearchMode } from '~~/shared/types' import { prettyLabel } from '~~/shared/stations' -/** - * Une ligne de résultat. Elle sert à *choisir* une ville, pas à l'étudier : nom, notoriété, - * durée du trajet le plus court, amplitude des départs. Le détail (horaires, réservation, - * dates de retour) vit dans la fiche de la carte, ouverte au clic. - * - * Une recherche à quatre semaines renvoie couramment 70 destinations, une exploration sur - * une semaine en renvoie 130 : tout déplier ferait plusieurs mètres de défilement. - */ +/** One result row: enough to *pick* a city, not to study it. A search commonly returns + * 70 to 130 of them, so the detail lives in the map popover. */ const props = defineProps<{ destination: Destination mode: SearchMode @@ -49,8 +43,8 @@ function formatDate(iso: string): string { @focus="emit('hover', destination.label)" @blur="emit('hover', null)" > - +
@@ -58,9 +52,7 @@ function formatDate(iso: string): string { {{ pop.stars }} - + popularityTier(props.destination.popularity)) const name = computed(() => prettyLabel(props.destination.label)) const originName = computed(() => prettyLabel(props.originLabel)) -/** En recherche inverse, le trajet part de la ville affichée et rejoint la gare cherchée. */ +/** In reverse search the trip starts from the displayed city and reaches the searched one. */ const isInbound = computed(() => props.mode === 'to') const fromName = computed(() => (isInbound.value ? name.value : originName.value)) const toName = computed(() => (isInbound.value ? originName.value : name.value)) @@ -57,7 +52,6 @@ function formatDate(iso: string): string { role="dialog" :aria-label="`Détails pour ${name}`" > -

{{ name }}

@@ -79,12 +73,10 @@ function formatDate(iso: string): string {
-

{{ fromName }} → {{ toName }}

- -