mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(settings): add copy button and revoke error toast to API tokens
The one-time API token rendered in a bare code block with no way to copy it short of selecting the text by hand — the worst place for copy friction since the value is shown exactly once. Add a copy-to-clipboard button beside the token with a success toast, reusing the clipboard-plus-toast pattern already used by the per-medication token row. Surface an inline check affordance for two seconds after a copy. Token revoke previously swallowed every failure: a non-OK response or a network error left the user with no signal the token was still live. Add an error toast on both paths so a failed revoke is visible.
This commit is contained in:
1 parent
4e2248dfff
commit
c24f42848e
7 files changed
+73
-7
No files matched your search
@@ -3506,6 +3506,10 @@
|
||||
"apiTokensDescription": "Erstelle API-Tokens für die externe Medikamenten-Einnahme (z.B. Shortcuts, Automationen).",
|
||||
"tokenNamePlaceholder": "Token-Name (z.B. iPhone Shortcut)",
|
||||
"tokenCreated": "Token erstellt — kopiere ihn jetzt! Er wird nicht erneut angezeigt.",
|
||||
"tokenCopy": "Token kopieren",
|
||||
"tokenCopied": "Token in die Zwischenablage kopiert",
|
||||
"tokenCopyFailed": "Token konnte nicht kopiert werden",
|
||||
"tokenRevokeFailed": "Token konnte nicht widerrufen werden. Bitte erneut versuchen.",
|
||||
"tokenRevoke": "Token widerrufen?",
|
||||
"tokenRevokeAction": "Token widerrufen",
|
||||
"tokenRevokeDescription": "Der Token wird unwiderruflich deaktiviert. Anwendungen, die ihn nutzen, verlieren den Zugriff.",
|
||||
|
||||
@@ -3506,6 +3506,10 @@
|
||||
"apiTokensDescription": "Create API tokens for external medication intake (e.g., Shortcuts, automations).",
|
||||
"tokenNamePlaceholder": "Token name (e.g., iPhone Shortcut)",
|
||||
"tokenCreated": "Token created — copy it now! It won't be shown again.",
|
||||
"tokenCopy": "Copy token",
|
||||
"tokenCopied": "Token copied to clipboard",
|
||||
"tokenCopyFailed": "Could not copy the token",
|
||||
"tokenRevokeFailed": "Could not revoke the token. Please try again.",
|
||||
"tokenRevoke": "Revoke token?",
|
||||
"tokenRevokeAction": "Revoke token",
|
||||
"tokenRevokeDescription": "The token will be permanently deactivated. Applications using it will lose access.",
|
||||
|
||||
@@ -3506,6 +3506,10 @@
|
||||
"apiTokensDescription": "Crea tokens de API para registrar tomas de medicación externas (p. ej., Atajos, automatizaciones).",
|
||||
"tokenNamePlaceholder": "Nombre del token (p. ej., Atajo de iPhone)",
|
||||
"tokenCreated": "Token creado — ¡cópialo ahora! No se mostrará de nuevo.",
|
||||
"tokenCopy": "Copiar token",
|
||||
"tokenCopied": "Token copiado al portapapeles",
|
||||
"tokenCopyFailed": "No se pudo copiar el token",
|
||||
"tokenRevokeFailed": "No se pudo revocar el token. Inténtalo de nuevo.",
|
||||
"tokenRevoke": "¿Revocar token?",
|
||||
"tokenRevokeAction": "Revocar token",
|
||||
"tokenRevokeDescription": "El token se desactivará de forma permanente. Las aplicaciones que lo usen perderán el acceso.",
|
||||
|
||||
@@ -3506,6 +3506,10 @@
|
||||
"apiTokensDescription": "Créez des jetons API pour enregistrer des prises de médicaments externes (par exemple Raccourcis, automatisations).",
|
||||
"tokenNamePlaceholder": "Nom du jeton (par exemple Raccourci iPhone)",
|
||||
"tokenCreated": "Jeton créé — copiez-le maintenant ! Il ne sera plus affiché.",
|
||||
"tokenCopy": "Copier le jeton",
|
||||
"tokenCopied": "Jeton copié dans le presse-papiers",
|
||||
"tokenCopyFailed": "Impossible de copier le jeton",
|
||||
"tokenRevokeFailed": "Impossible de révoquer le jeton. Veuillez réessayer.",
|
||||
"tokenRevoke": "Révoquer le jeton ?",
|
||||
"tokenRevokeAction": "Révoquer le jeton",
|
||||
"tokenRevokeDescription": "Le jeton sera désactivé définitivement. Les applications qui l’utilisent perdront l’accès.",
|
||||
|
||||
@@ -3506,6 +3506,10 @@
|
||||
"apiTokensDescription": "Crea token API per registrare assunzioni di farmaci esterne (ad es. Comandi rapidi, automazioni).",
|
||||
"tokenNamePlaceholder": "Nome del token (ad es. Comando rapido iPhone)",
|
||||
"tokenCreated": "Token creato — copialo ora! Non verrà mostrato di nuovo.",
|
||||
"tokenCopy": "Copia token",
|
||||
"tokenCopied": "Token copiato negli appunti",
|
||||
"tokenCopyFailed": "Impossibile copiare il token",
|
||||
"tokenRevokeFailed": "Impossibile revocare il token. Riprova.",
|
||||
"tokenRevoke": "Revocare il token?",
|
||||
"tokenRevokeAction": "Revoca token",
|
||||
"tokenRevokeDescription": "Il token verrà disattivato definitivamente. Le applicazioni che lo usano perderanno l’accesso.",
|
||||
|
||||
@@ -3506,6 +3506,10 @@
|
||||
"apiTokensDescription": "Twórz tokeny API do zewnętrznego rejestrowania przyjęć leków (np. Skróty, automatyzacje).",
|
||||
"tokenNamePlaceholder": "Nazwa tokena (np. Skrót iPhone)",
|
||||
"tokenCreated": "Token utworzony — skopiuj go teraz! Nie zostanie pokazany ponownie.",
|
||||
"tokenCopy": "Kopiuj token",
|
||||
"tokenCopied": "Token skopiowany do schowka",
|
||||
"tokenCopyFailed": "Nie udało się skopiować tokenu",
|
||||
"tokenRevokeFailed": "Nie udało się unieważnić tokenu. Spróbuj ponownie.",
|
||||
"tokenRevoke": "Cofnąć token?",
|
||||
"tokenRevokeAction": "Unieważnij token",
|
||||
"tokenRevokeDescription": "Token zostanie trwale dezaktywowany. Aplikacje, które go używają, stracą dostęp.",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronDown, Key, Loader2, Trash2 } from "lucide-react";
|
||||
import { Check, ChevronDown, Copy, Key, Loader2, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -165,6 +166,7 @@ function ApiTokensCard() {
|
||||
const [creating, setCreating] = useState(false);
|
||||
const [tokenMsg, setTokenMsg] = useState<string | null>(null);
|
||||
const [showRevokedTokens, setShowRevokedTokens] = useState(false);
|
||||
const [tokenCopied, setTokenCopied] = useState(false);
|
||||
|
||||
const { data: tokens } = useQuery({
|
||||
queryKey: queryKeys.tokens(),
|
||||
@@ -203,10 +205,30 @@ function ApiTokensCard() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCopyToken() {
|
||||
if (!newToken) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(newToken);
|
||||
setTokenCopied(true);
|
||||
toast.success(t("settings.tokenCopied"));
|
||||
// Revert the inline check affordance after a short beat so a
|
||||
// second copy reads as a fresh action.
|
||||
setTimeout(() => setTokenCopied(false), 2_000);
|
||||
} catch {
|
||||
toast.error(t("settings.tokenCopyFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRevoke(tokenId: string) {
|
||||
const res = await fetch(`/api/tokens/${tokenId}`, { method: "DELETE" });
|
||||
if (res.ok) {
|
||||
try {
|
||||
const res = await fetch(`/api/tokens/${tokenId}`, { method: "DELETE" });
|
||||
if (!res.ok) {
|
||||
toast.error(t("settings.tokenRevokeFailed"));
|
||||
return;
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.tokens() });
|
||||
} catch {
|
||||
toast.error(t("settings.tokenRevokeFailed"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,13 +288,33 @@ function ApiTokensCard() {
|
||||
</form>
|
||||
|
||||
{newToken && (
|
||||
<div className="bg-dracula-green/10 rounded-lg p-3 text-sm">
|
||||
<div
|
||||
className="bg-dracula-green/10 rounded-lg p-3 text-sm"
|
||||
data-slot="settings-api-token-created"
|
||||
>
|
||||
<p className="text-dracula-green mb-1 font-medium">
|
||||
{t("settings.tokenCreated")}
|
||||
</p>
|
||||
<code className="bg-muted block rounded p-2 font-mono text-xs break-all">
|
||||
{newToken}
|
||||
</code>
|
||||
<div className="flex items-start gap-2">
|
||||
<code className="bg-muted block flex-1 rounded p-2 font-mono text-xs break-all">
|
||||
{newToken}
|
||||
</code>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9 shrink-0"
|
||||
onClick={() => void handleCopyToken()}
|
||||
aria-label={t("settings.tokenCopy")}
|
||||
data-slot="settings-api-token-copy"
|
||||
>
|
||||
{tokenCopied ? (
|
||||
<Check className="text-dracula-green h-3.5 w-3.5" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
Reference in new issue
Block a user