feat: dismissable banner with session-storage memory (#81)
Adds a close button and session-scoped dismissal flag so the banner can be acknowledged for the current run but reappears on the next app launch if the fallback is still active — matches the #81 acceptance criterion. - sessionStorage key survives page navigation within the run, is cleared on app restart. - Graceful on storage quota errors. - New `common.close` i18n key (FR: "Fermer", EN: "Close") used as the aria-label of the close button.
This commit is contained in:
parent
3b1c41c48e
commit
9a9d3c89b9
3 changed files with 37 additions and 5 deletions
|
|
@ -1,11 +1,23 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ShieldAlert } from "lucide-react";
|
import { ShieldAlert, X } from "lucide-react";
|
||||||
import { getTokenStoreMode, TokenStoreMode } from "../../services/authService";
|
import { getTokenStoreMode, TokenStoreMode } from "../../services/authService";
|
||||||
|
|
||||||
|
// Per-session dismissal flag. Kept in sessionStorage so the banner
|
||||||
|
// returns on the next app launch if the fallback condition still
|
||||||
|
// holds — this matches the acceptance criteria from issue #81.
|
||||||
|
const DISMISS_KEY = "tokenStoreFallbackBannerDismissed";
|
||||||
|
|
||||||
export default function TokenStoreFallbackBanner() {
|
export default function TokenStoreFallbackBanner() {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [mode, setMode] = useState<TokenStoreMode | null>(null);
|
const [mode, setMode] = useState<TokenStoreMode | null>(null);
|
||||||
|
const [dismissed, setDismissed] = useState<boolean>(() => {
|
||||||
|
try {
|
||||||
|
return sessionStorage.getItem(DISMISS_KEY) === "1";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
@ -21,12 +33,22 @@ export default function TokenStoreFallbackBanner() {
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (mode !== "file") return null;
|
if (mode !== "file" || dismissed) return null;
|
||||||
|
|
||||||
|
const dismiss = () => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(DISMISS_KEY, "1");
|
||||||
|
} catch {
|
||||||
|
// Ignore storage errors — the banner will simply hide for the
|
||||||
|
// remainder of this render cycle via state.
|
||||||
|
}
|
||||||
|
setDismissed(true);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-start gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4">
|
<div className="flex items-start gap-3 rounded-xl border border-amber-500/40 bg-amber-500/10 p-4">
|
||||||
<ShieldAlert size={20} className="mt-0.5 shrink-0 text-amber-500" />
|
<ShieldAlert size={20} className="mt-0.5 shrink-0 text-amber-500" />
|
||||||
<div className="space-y-1 text-sm">
|
<div className="flex-1 space-y-1 text-sm">
|
||||||
<p className="font-semibold text-[var(--foreground)]">
|
<p className="font-semibold text-[var(--foreground)]">
|
||||||
{t("account.tokenStore.fallback.title")}
|
{t("account.tokenStore.fallback.title")}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -34,6 +56,14 @@ export default function TokenStoreFallbackBanner() {
|
||||||
{t("account.tokenStore.fallback.description")}
|
{t("account.tokenStore.fallback.description")}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={dismiss}
|
||||||
|
aria-label={t("common.close")}
|
||||||
|
className="shrink-0 text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -847,7 +847,8 @@
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"darkMode": "Dark mode",
|
"darkMode": "Dark mode",
|
||||||
"lightMode": "Light mode"
|
"lightMode": "Light mode",
|
||||||
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"title": "License",
|
"title": "License",
|
||||||
|
|
|
||||||
|
|
@ -847,7 +847,8 @@
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
"darkMode": "Mode sombre",
|
"darkMode": "Mode sombre",
|
||||||
"lightMode": "Mode clair"
|
"lightMode": "Mode clair",
|
||||||
|
"close": "Fermer"
|
||||||
},
|
},
|
||||||
"license": {
|
"license": {
|
||||||
"title": "Licence",
|
"title": "Licence",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue