diff --git a/src/components/shared/success-modal.tsx b/src/components/shared/success-modal.tsx
index c30ef8b..2a07db2 100644
--- a/src/components/shared/success-modal.tsx
+++ b/src/components/shared/success-modal.tsx
@@ -1,7 +1,7 @@
"use client";
-import React, { useEffect, useState } from "react";
-import { CheckCircle2, X } from "lucide-react";
+import React, { useCallback, useEffect, useState } from "react";
+import { AlertCircle, CheckCircle2, X } from "lucide-react";
interface SuccessModalProps {
isOpen: boolean;
@@ -9,35 +9,57 @@ interface SuccessModalProps {
title: string;
message: string;
duration?: number;
+ /**
+ * `error` change l'icône, l'accent et **désactive la fermeture automatique** :
+ * un message d'échec doit rester à l'écran le temps d'être lu, et il ne doit
+ * pas s'afficher sous une coche verte de succès.
+ */
+ variant?: "success" | "error";
+ /** Action secondaire optionnelle (ex. « Recharger la page »). */
+ action?: { label: string; onClick: () => void };
}
+/** Durée de l'animation de sortie, alignée sur les classes `animate-out`. */
+const EXIT_MS = 200;
+
export function SuccessModal({
isOpen,
onClose,
title,
message,
- duration = 3000
+ duration = 3000,
+ variant = "success",
+ action,
}: SuccessModalProps) {
- const [isVisible, setIsVisible] = useState(false);
+ // Seul l'état de SORTIE est porté par un state : l'entrée est jouée par CSS
+ // au montage (`animate-in`), ce qui évite un setState synchrone dans l'effet.
+ const [closing, setClosing] = useState(false);
+ const isError = variant === "error";
+
+ const handleClose = useCallback(() => {
+ setClosing(true);
+ setTimeout(() => {
+ setClosing(false);
+ onClose();
+ }, EXIT_MS);
+ }, [onClose]);
useEffect(() => {
- if (isOpen) {
- setIsVisible(true);
- const timer = setTimeout(() => handleClose(), duration);
- return () => clearTimeout(timer);
- }
- }, [isOpen, duration]);
+ // Une erreur ne disparaît pas toute seule : l'utilisateur la ferme.
+ if (!isOpen || isError) return;
+ const timer = setTimeout(() => handleClose(), duration);
+ return () => clearTimeout(timer);
+ }, [isOpen, duration, isError, handleClose]);
- const handleClose = () => {
- setIsVisible(false);
- setTimeout(onClose, 250);
- };
+ if (!isOpen) return null;
- if (!isOpen && !isVisible) return null;
+ const state = closing ? "closed" : "open";
+ const Icon = isError ? AlertCircle : CheckCircle2;
return (
{/* Backdrop */}
{/* Accent bar */}
-
+
{/* Close button */}
diff --git a/src/features/grid/components/floating-summary-bar.tsx b/src/features/grid/components/floating-summary-bar.tsx
index 07df5a8..dbf5b7d 100644
--- a/src/features/grid/components/floating-summary-bar.tsx
+++ b/src/features/grid/components/floating-summary-bar.tsx
@@ -7,6 +7,7 @@ import { Loader2, CheckCircle, AlertCircle, RotateCcw, Camera, ChevronDown } fro
import { useMemo, useState, useTransition } from "react";
import { saveSnapshot } from "@/features/snapshots/api/save-snapshot";
import { SuccessModal } from "@/components/shared/success-modal";
+import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action";
import {
DropdownMenu,
DropdownMenuContent,
@@ -37,7 +38,13 @@ export function FloatingSummaryBar() {
const [isPending, startTransition] = useTransition();
const [isSavingSnapshot, setIsSavingSnapshot] = useState(false);
const [saveStatus, setSaveStatus] = useState<"idle" | "success" | "error">("idle");
- const [modal, setModal] = useState<{ isOpen: boolean, title: string, message: string }>({
+ const [modal, setModal] = useState<{
+ isOpen: boolean;
+ title: string;
+ message: string;
+ variant?: "success" | "error";
+ action?: { label: string; onClick: () => void };
+ }>({
isOpen: false,
title: "",
message: ""
@@ -118,12 +125,26 @@ export function FloatingSummaryBar() {
} catch (err) {
console.error(err);
if (!labelOverride) {
- const msg = err instanceof Error ? err.message : String(err);
- setModal({
- isOpen: true,
- title: "Erreur Snapshot",
- message: `Impossible de créer le snapshot : ${msg}`
- });
+ // Onglet resté ouvert pendant un déploiement : les identifiants de
+ // Server Action ont changé côté serveur. Rien à réparer côté
+ // métier, il faut recharger le bundle client.
+ if (isStaleServerActionError(err)) {
+ setModal({
+ isOpen: true,
+ title: "Page à recharger",
+ message: STALE_ACTION_MESSAGE,
+ variant: "error",
+ action: { label: "Recharger la page", onClick: () => window.location.reload() },
+ });
+ } else {
+ const msg = err instanceof Error ? err.message : String(err);
+ setModal({
+ isOpen: true,
+ title: "Erreur Snapshot",
+ message: `Impossible de créer le snapshot : ${msg}`,
+ variant: "error",
+ });
+ }
}
} finally {
setIsSavingSnapshot(false);
@@ -327,6 +348,8 @@ export function FloatingSummaryBar() {
onClose={() => setModal(prev => ({ ...prev, isOpen: false }))}
title={modal.title}
message={modal.message}
+ variant={modal.variant}
+ action={modal.action}
/>
);
diff --git a/src/features/snapshots/components/snapshot-list.tsx b/src/features/snapshots/components/snapshot-list.tsx
index 8281ee3..5f072bd 100644
--- a/src/features/snapshots/components/snapshot-list.tsx
+++ b/src/features/snapshots/components/snapshot-list.tsx
@@ -15,6 +15,7 @@ import {
Loader2,
} from "lucide-react";
import { SuccessModal } from "@/components/shared/success-modal";
+import { isStaleServerActionError, STALE_ACTION_MESSAGE } from "@/lib/stale-action";
import { ConfirmModal } from "@/components/shared/confirm-modal";
const formatDate = (date: Date) => {
@@ -36,12 +37,34 @@ export function SnapshotList({ type }: SnapshotListProps) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [isDeleting, setIsDeleting] = useState(null);
- const [modal, setModal] = useState<{ isOpen: boolean, title: string, message: string }>({
+ const [modal, setModal] = useState<{
+ isOpen: boolean;
+ title: string;
+ message: string;
+ variant?: "success" | "error";
+ action?: { label: string; onClick: () => void };
+ }>({
isOpen: false,
title: "",
message: ""
});
+ /**
+ * Onglet resté ouvert pendant un déploiement : les identifiants de Server
+ * Action ont changé côté serveur. On propose le rechargement au lieu
+ * d'afficher une erreur technique incompréhensible.
+ */
+ const modalErreur = (err: unknown, titre: string, message: string) =>
+ isStaleServerActionError(err)
+ ? {
+ isOpen: true,
+ title: "Page à recharger",
+ message: STALE_ACTION_MESSAGE,
+ variant: "error" as const,
+ action: { label: "Recharger la page", onClick: () => window.location.reload() },
+ }
+ : { isOpen: true, title: titre, message, variant: "error" as const };
+
const [confirmModal, setConfirmModal] = useState<{
isOpen: boolean;
title: string;
@@ -59,7 +82,7 @@ export function SnapshotList({ type }: SnapshotListProps) {
const data = await getSnapshots(type);
setSnapshots(data);
} catch (err) {
- setError("Impossible de charger les données.");
+ setError(isStaleServerActionError(err) ? STALE_ACTION_MESSAGE : "Impossible de charger les données.");
} finally {
setLoading(false);
}
@@ -83,10 +106,10 @@ export function SnapshotList({ type }: SnapshotListProps) {
setSnapshots(prev => prev.filter(s => s.id !== id));
setModal({ isOpen: true, title: "Action Réussie", message: "L'élément a été supprimé de votre historique." });
} else {
- setModal({ isOpen: true, title: "Erreur", message: "Impossible de supprimer l'élément." });
+ setModal({ isOpen: true, title: "Erreur", message: "Impossible de supprimer l'élément.", variant: "error" });
}
} catch (err) {
- setModal({ isOpen: true, title: "Erreur Technique", message: "Une erreur est survenue lors de la suppression." });
+ setModal(modalErreur(err, "Erreur Technique", "Une erreur est survenue lors de la suppression."));
} finally {
setIsDeleting(null);
}
@@ -215,6 +238,8 @@ export function SnapshotList({ type }: SnapshotListProps) {
onClose={() => setModal(prev => ({ ...prev, isOpen: false }))}
title={modal.title}
message={modal.message}
+ variant={modal.variant}
+ action={modal.action}
/>