feat(web): toast "Nouvelle version" persistant + lien vers le changelog
Composant `<VersionToast/>` monté au root de la SPA qui s'affiche quand l'utilisateur ouvre l'app sur une version plus récente que la dernière qu'il a vue. Mécanique : - Source de vérité = `apps/web/src/version.ts` (constante semver, à bumper manuellement à chaque release, en même temps que l'ajout du .md correspondant dans `apps/landing/src/content/changelog/`). - Comparaison à `localStorage["rubis:last-seen-version"]` au mount. - 1re visite (clé absente) → on enregistre la version courante en silence, pas de toast (sinon spam d'onboarding). - Version identique à la dernière vue → rien. - Version différente → toast Sonner persistant (`duration: Infinity`) avec icône Sparkles rubis et action "Voir les nouveautés ↗" qui ouvre `rubis.pro/changelog#<version>` dans un nouvel onglet. Au moment de l'affichage on enregistre la version comme vue — donc même si l'user ferme sans cliquer, plus de toast pour cette version (il a été informé). - localStorage indisponible (private mode) → fail silent. Version initiale : `1.10.0` (remerciement automatique au client, dernière entrée du changelog). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
fc0d13e955
commit
847e7a3fc5
70
apps/web/src/components/version-toast.tsx
Normal file
70
apps/web/src/components/version-toast.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
import { APP_VERSION, CHANGELOG_URL } from "../version";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "rubis:last-seen-version";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Affiche un toast persistant quand l'utilisateur ouvre l'app sur une version
|
||||||
|
* plus récente que la dernière qu'il a vue (lu/comparé via `localStorage`).
|
||||||
|
*
|
||||||
|
* Comportement :
|
||||||
|
* - 1re visite (pas de clé en localStorage) → on enregistre la version
|
||||||
|
* courante en silence, pas de toast (sinon spam d'onboarding).
|
||||||
|
* - Visite suivante, version identique → rien.
|
||||||
|
* - Visite suivante, version différente → toast persistant (duration: Infinity)
|
||||||
|
* avec action "Voir les nouveautés" qui ouvre `rubis.pro/changelog#<version>`
|
||||||
|
* dans un nouvel onglet. Au moment de l'affichage on enregistre la version
|
||||||
|
* comme vue — donc même si l'user ferme le toast sans cliquer, il ne le
|
||||||
|
* reverra pas (il a été informé).
|
||||||
|
*
|
||||||
|
* localStorage indisponible (private mode, restrictions) → fail silent, pas
|
||||||
|
* de toast et pas de crash.
|
||||||
|
*/
|
||||||
|
export function VersionToast() {
|
||||||
|
useEffect(() => {
|
||||||
|
let seen: string | null = null;
|
||||||
|
try {
|
||||||
|
seen = localStorage.getItem(STORAGE_KEY);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen === null) {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, APP_VERSION);
|
||||||
|
} catch {
|
||||||
|
// Tant pis — on ne montrera pas de toast non plus.
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen === APP_VERSION) return;
|
||||||
|
|
||||||
|
toast.message(`Nouvelle version v${APP_VERSION}`, {
|
||||||
|
description: "Découvrez ce qui change.",
|
||||||
|
duration: Infinity,
|
||||||
|
icon: <Sparkles className="size-4 text-rubis" aria-hidden />,
|
||||||
|
action: {
|
||||||
|
label: "Voir les nouveautés ↗",
|
||||||
|
onClick: () => {
|
||||||
|
window.open(
|
||||||
|
`${CHANGELOG_URL}#${APP_VERSION}`,
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, APP_VERSION);
|
||||||
|
} catch {
|
||||||
|
// Pareil — on a quand même informé l'user via le toast, c'est l'essentiel.
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ import type { QueryClient } from "@tanstack/react-query";
|
|||||||
import { Toaster } from "sonner";
|
import { Toaster } from "sonner";
|
||||||
import { lazy, Suspense } from "react";
|
import { lazy, Suspense } from "react";
|
||||||
|
|
||||||
|
import { VersionToast } from "../components/version-toast";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Devtools — chargés uniquement en dev pour ne pas alourdir le bundle prod.
|
* Devtools — chargés uniquement en dev pour ne pas alourdir le bundle prod.
|
||||||
* Cf. /docs/tech/frontend.md §4.
|
* Cf. /docs/tech/frontend.md §4.
|
||||||
@ -47,6 +49,7 @@ function RootLayout() {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<VersionToast />
|
||||||
{import.meta.env.DEV && (
|
{import.meta.env.DEV && (
|
||||||
<Suspense fallback={null}>
|
<Suspense fallback={null}>
|
||||||
<TanStackRouterDevtools position="bottom-left" />
|
<TanStackRouterDevtools position="bottom-left" />
|
||||||
|
|||||||
17
apps/web/src/version.ts
Normal file
17
apps/web/src/version.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Version courante de l'app SPA, source de vérité pour :
|
||||||
|
* - le toast "Nouvelle version" (<VersionToast/>) qui compare cette valeur à
|
||||||
|
* `localStorage["rubis:last-seen-version"]` au mount du root
|
||||||
|
* - le release tag Sentry (à brancher si pas déjà fait)
|
||||||
|
*
|
||||||
|
* À bumper manuellement à chaque release, EN MÊME TEMPS qu'on ajoute le .md
|
||||||
|
* correspondant dans `apps/landing/src/content/changelog/<version>.md`. La
|
||||||
|
* même PR contient les deux — sinon le toast pointe sur une ancre absente.
|
||||||
|
*
|
||||||
|
* Convention : semver (major.minor.patch). Le toast affiche `v${APP_VERSION}`
|
||||||
|
* et linke vers `https://rubis.pro/changelog#${APP_VERSION}`.
|
||||||
|
*/
|
||||||
|
export const APP_VERSION = "1.10.0";
|
||||||
|
|
||||||
|
/** URL absolue de la page changelog (utilisée par le toast). */
|
||||||
|
export const CHANGELOG_URL = "https://rubis.pro/changelog";
|
||||||
Loading…
x
Reference in New Issue
Block a user