feat(settings): drop the moodLog integration from the integrations page

Remove the moodLog card, its render, and the moodLog-only UI plumbing
from the integrations settings — the integration is no longer offered.
The backing columns, routes, and status aggregator entry stay in place
for a later dedicated removal. Also shorten the integration save button
from "Save credentials" to "Save" across all locales, and add the
nav account-settings label.
This commit is contained in:
Marc-André Bombeck committed 2026-06-06 23:14:34 +02:00
1 parent 79ce836335
commit a56e6fe610
7 files changed
+24 -391

No files matched your search

+4 -3
View File
@@ -212,6 +212,7 @@
"themeLight": "Hell",
"collapseSidebar": "Einklappen",
"userMenu": "Benutzermenü",
"accountSettings": "Kontoeinstellungen",
"mood": "Stimmung",
"workouts": "Workouts",
"skipToContent": "Zum Inhalt springen",
@@ -3583,7 +3584,7 @@
"withingsCredentialsSaved": "Zugangsdaten gespeichert",
"withingsCredentialsSavedPlaceholder": "Gespeichert — neue eingeben zum Ersetzen",
"withingsCredentialsSavedPlaceholderSecret": "Gespeichert — neues eingeben zum Ersetzen",
"withingsSaveCredentials": "Zugangsdaten speichern",
"withingsSaveCredentials": "Speichern",
"configured": "Konfiguriert",
"withingsSync": "Jetzt synchronisieren",
"withingsFullSync": "Alle Daten synchronisieren",
@@ -3608,7 +3609,7 @@
"whoopCredentialsSaved": "Zugangsdaten gespeichert",
"whoopCredentialsSavedPlaceholder": "Gespeichert — neu eingeben zum Ersetzen",
"whoopCredentialsSavedPlaceholderSecret": "Gespeichert — neu eingeben zum Ersetzen",
"whoopSaveCredentials": "Zugangsdaten speichern",
"whoopSaveCredentials": "Speichern",
"whoopSync": "Jetzt synchronisieren",
"whoopFullSync": "Alle Daten synchronisieren",
"whoopFullSyncTitle": "Vollständige Synchronisierung?",
@@ -3635,7 +3636,7 @@
"fitbitCredentialsSaved": "Zugangsdaten gespeichert",
"fitbitCredentialsSavedPlaceholder": "Gespeichert — neu eingeben zum Ersetzen",
"fitbitCredentialsSavedPlaceholderSecret": "Gespeichert — neu eingeben zum Ersetzen",
"fitbitSaveCredentials": "Zugangsdaten speichern",
"fitbitSaveCredentials": "Speichern",
"fitbitSync": "Jetzt synchronisieren",
"fitbitFullSync": "Alle Daten synchronisieren",
"fitbitFullSyncTitle": "Vollständige Synchronisierung?",
+4 -3
View File
@@ -212,6 +212,7 @@
"themeLight": "Light",
"collapseSidebar": "Collapse",
"userMenu": "User menu",
"accountSettings": "Account settings",
"mood": "Mood",
"workouts": "Workouts",
"skipToContent": "Skip to content",
@@ -3583,7 +3584,7 @@
"withingsCredentialsSaved": "Credentials saved",
"withingsCredentialsSavedPlaceholder": "Saved — enter new to replace",
"withingsCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
"withingsSaveCredentials": "Save credentials",
"withingsSaveCredentials": "Save",
"configured": "Configured",
"withingsSync": "Sync now",
"withingsFullSync": "Sync all data",
@@ -3608,7 +3609,7 @@
"whoopCredentialsSaved": "Credentials saved",
"whoopCredentialsSavedPlaceholder": "Saved — enter new to replace",
"whoopCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
"whoopSaveCredentials": "Save credentials",
"whoopSaveCredentials": "Save",
"whoopSync": "Sync now",
"whoopFullSync": "Sync all data",
"whoopFullSyncTitle": "Full synchronization?",
@@ -3635,7 +3636,7 @@
"fitbitCredentialsSaved": "Credentials saved",
"fitbitCredentialsSavedPlaceholder": "Saved — enter new to replace",
"fitbitCredentialsSavedPlaceholderSecret": "Saved — enter new to replace",
"fitbitSaveCredentials": "Save credentials",
"fitbitSaveCredentials": "Save",
"fitbitSync": "Sync now",
"fitbitFullSync": "Sync all data",
"fitbitFullSyncTitle": "Full synchronization?",
+4 -3
View File
@@ -212,6 +212,7 @@
"themeLight": "Claro",
"collapseSidebar": "Contraer",
"userMenu": "Menú de usuario",
"accountSettings": "Ajustes de cuenta",
"mood": "Estado de ánimo",
"workouts": "Entrenamientos",
"skipToContent": "Saltar al contenido",
@@ -3583,7 +3584,7 @@
"withingsCredentialsSaved": "Credenciales guardadas",
"withingsCredentialsSavedPlaceholder": "Guardado — introduce uno nuevo para reemplazarlo",
"withingsCredentialsSavedPlaceholderSecret": "Guardado — introduce uno nuevo para reemplazarlo",
"withingsSaveCredentials": "Guardar credenciales",
"withingsSaveCredentials": "Guardar",
"configured": "Configurado",
"withingsSync": "Sincronizar ahora",
"withingsFullSync": "Sincronizar todos los datos",
@@ -3608,7 +3609,7 @@
"whoopCredentialsSaved": "Credenciales guardadas",
"whoopCredentialsSavedPlaceholder": "Guardado — introduce nuevas para reemplazar",
"whoopCredentialsSavedPlaceholderSecret": "Guardado — introduce nuevas para reemplazar",
"whoopSaveCredentials": "Guardar credenciales",
"whoopSaveCredentials": "Guardar",
"whoopSync": "Sincronizar ahora",
"whoopFullSync": "Sincronizar todos los datos",
"whoopFullSyncTitle": "¿Sincronización completa?",
@@ -3635,7 +3636,7 @@
"fitbitCredentialsSaved": "Credenciales guardadas",
"fitbitCredentialsSavedPlaceholder": "Guardado: introduce nuevas para reemplazar",
"fitbitCredentialsSavedPlaceholderSecret": "Guardado: introduce nuevas para reemplazar",
"fitbitSaveCredentials": "Guardar credenciales",
"fitbitSaveCredentials": "Guardar",
"fitbitSync": "Sincronizar ahora",
"fitbitFullSync": "Sincronizar todos los datos",
"fitbitFullSyncTitle": "¿Sincronización completa?",
+4 -3
View File
@@ -212,6 +212,7 @@
"themeLight": "Clair",
"collapseSidebar": "Réduire",
"userMenu": "Menu utilisateur",
"accountSettings": "Paramètres du compte",
"mood": "Humeur",
"workouts": "Entraînements",
"skipToContent": "Aller au contenu",
@@ -3583,7 +3584,7 @@
"withingsCredentialsSaved": "Identifiants enregistrés",
"withingsCredentialsSavedPlaceholder": "Enregistré — saisissez-en un nouveau pour remplacer",
"withingsCredentialsSavedPlaceholderSecret": "Enregistré — saisissez-en un nouveau pour remplacer",
"withingsSaveCredentials": "Enregistrer les identifiants",
"withingsSaveCredentials": "Enregistrer",
"configured": "Configuré",
"withingsSync": "Synchroniser maintenant",
"withingsFullSync": "Synchroniser toutes les données",
@@ -3608,7 +3609,7 @@
"whoopCredentialsSaved": "Identifiants enregistrés",
"whoopCredentialsSavedPlaceholder": "Enregistré — saisir de nouveaux pour remplacer",
"whoopCredentialsSavedPlaceholderSecret": "Enregistré — saisir de nouveaux pour remplacer",
"whoopSaveCredentials": "Enregistrer les identifiants",
"whoopSaveCredentials": "Enregistrer",
"whoopSync": "Synchroniser",
"whoopFullSync": "Synchroniser toutes les données",
"whoopFullSyncTitle": "Synchronisation complète ?",
@@ -3635,7 +3636,7 @@
"fitbitCredentialsSaved": "Identifiants enregistrés",
"fitbitCredentialsSavedPlaceholder": "Enregistré — saisissez-en de nouveaux pour remplacer",
"fitbitCredentialsSavedPlaceholderSecret": "Enregistré — saisissez-en de nouveaux pour remplacer",
"fitbitSaveCredentials": "Enregistrer les identifiants",
"fitbitSaveCredentials": "Enregistrer",
"fitbitSync": "Synchroniser maintenant",
"fitbitFullSync": "Tout synchroniser",
"fitbitFullSyncTitle": "Synchronisation complète ?",
+4 -3
View File
@@ -212,6 +212,7 @@
"themeLight": "Chiaro",
"collapseSidebar": "Riduci",
"userMenu": "Menu utente",
"accountSettings": "Impostazioni account",
"mood": "Umore",
"workouts": "Allenamenti",
"skipToContent": "Vai al contenuto",
@@ -3583,7 +3584,7 @@
"withingsCredentialsSaved": "Credenziali salvate",
"withingsCredentialsSavedPlaceholder": "Salvato — inseriscine uno nuovo per sostituirlo",
"withingsCredentialsSavedPlaceholderSecret": "Salvato — inseriscine uno nuovo per sostituirlo",
"withingsSaveCredentials": "Salva credenziali",
"withingsSaveCredentials": "Salva",
"configured": "Configurato",
"withingsSync": "Sincronizza ora",
"withingsFullSync": "Sincronizza tutti i dati",
@@ -3608,7 +3609,7 @@
"whoopCredentialsSaved": "Credenziali salvate",
"whoopCredentialsSavedPlaceholder": "Salvato — inserisci nuove per sostituire",
"whoopCredentialsSavedPlaceholderSecret": "Salvato — inserisci nuove per sostituire",
"whoopSaveCredentials": "Salva credenziali",
"whoopSaveCredentials": "Salva",
"whoopSync": "Sincronizza ora",
"whoopFullSync": "Sincronizza tutti i dati",
"whoopFullSyncTitle": "Sincronizzazione completa?",
@@ -3635,7 +3636,7 @@
"fitbitCredentialsSaved": "Credenziali salvate",
"fitbitCredentialsSavedPlaceholder": "Salvato — inserisci nuove per sostituire",
"fitbitCredentialsSavedPlaceholderSecret": "Salvato — inserisci nuove per sostituire",
"fitbitSaveCredentials": "Salva credenziali",
"fitbitSaveCredentials": "Salva",
"fitbitSync": "Sincronizza ora",
"fitbitFullSync": "Sincronizza tutti i dati",
"fitbitFullSyncTitle": "Sincronizzazione completa?",
+4 -3
View File
@@ -212,6 +212,7 @@
"themeLight": "Jasny",
"collapseSidebar": "Zwiń",
"userMenu": "Menu użytkownika",
"accountSettings": "Ustawienia konta",
"mood": "Nastrój",
"workouts": "Treningi",
"skipToContent": "Przejdź do treści",
@@ -3583,7 +3584,7 @@
"withingsCredentialsSaved": "Dane logowania zapisane",
"withingsCredentialsSavedPlaceholder": "Zapisano — wprowadź nowy, aby zastąpić",
"withingsCredentialsSavedPlaceholderSecret": "Zapisano — wprowadź nowy, aby zastąpić",
"withingsSaveCredentials": "Zapisz dane logowania",
"withingsSaveCredentials": "Zapisz",
"configured": "Skonfigurowano",
"withingsSync": "Synchronizuj teraz",
"withingsFullSync": "Synchronizuj wszystkie dane",
@@ -3608,7 +3609,7 @@
"whoopCredentialsSaved": "Dane zapisane",
"whoopCredentialsSavedPlaceholder": "Zapisano — wpisz nowe, aby zastąpić",
"whoopCredentialsSavedPlaceholderSecret": "Zapisano — wpisz nowe, aby zastąpić",
"whoopSaveCredentials": "Zapisz dane",
"whoopSaveCredentials": "Zapisz",
"whoopSync": "Synchronizuj teraz",
"whoopFullSync": "Synchronizuj wszystkie dane",
"whoopFullSyncTitle": "Pełna synchronizacja?",
@@ -3635,7 +3636,7 @@
"fitbitCredentialsSaved": "Dane uwierzytelniające zapisane",
"fitbitCredentialsSavedPlaceholder": "Zapisano — wpisz nowe, aby zastąpić",
"fitbitCredentialsSavedPlaceholderSecret": "Zapisano — wpisz nowe, aby zastąpić",
"fitbitSaveCredentials": "Zapisz dane uwierzytelniające",
"fitbitSaveCredentials": "Zapisz",
"fitbitSync": "Synchronizuj teraz",
"fitbitFullSync": "Synchronizuj wszystkie dane",
"fitbitFullSyncTitle": "Pełna synchronizacja?",
@@ -10,7 +10,6 @@ import {
Loader2,
RefreshCw,
Save,
Smile,
Unlink,
} from "lucide-react";
@@ -42,14 +41,6 @@ import {
queryKeys,
} from "@/lib/query-keys";
interface GlobalServiceAvailability {
telegramGlobal: boolean;
ntfyGlobal: boolean;
webPushGlobal: boolean;
apiGlobal: boolean;
moodLogGlobal: boolean;
}
// v1.4.15 Phase B2: shared status payload for both integration cards.
// v1.4.19 Phase A5: the redundant in-card status banner is gone — the
// IntegrationStatusPill now owns state + last-sync presentation, and
@@ -183,23 +174,11 @@ export function IntegrationsSection() {
const { t } = useTranslations();
const { isAuthenticated } = useAuth();
const { data: globalServices } = useQuery({
queryKey: queryKeys.settingsGlobalServices(),
queryFn: async () => {
const res = await fetch("/api/settings/global-services");
if (!res.ok) throw new Error("Failed");
return (await res.json()).data as GlobalServiceAvailability;
},
enabled: isAuthenticated,
});
const { data: integrationStatus } = useIntegrationStatuses(isAuthenticated);
const moodLogEnabled = globalServices?.moodLogGlobal ?? true;
const withingsViewModel = pickStatus(integrationStatus, "withings");
const whoopViewModel = pickStatus(integrationStatus, "whoop");
const fitbitViewModel = pickStatus(integrationStatus, "fitbit");
const moodLogViewModel = pickStatus(integrationStatus, "moodlog");
return (
<section
@@ -221,7 +200,6 @@ export function IntegrationsSection() {
<WithingsCard viewModel={withingsViewModel} />
<WhoopCard viewModel={whoopViewModel} />
<FitbitCard viewModel={fitbitViewModel} />
{moodLogEnabled && <MoodLogCard viewModel={moodLogViewModel} />}
</section>
);
}
@@ -1545,354 +1523,3 @@ function FitbitCard({
</div>
);
}
function MoodLogCard({
viewModel,
}: {
viewModel: IntegrationStatusViewModel | undefined;
}) {
const { t } = useTranslations();
const queryClient = useQueryClient();
const [url, setUrl] = useState("");
const [apiKey, setApiKey] = useState("");
const [saving, setSaving] = useState(false);
const [syncing, setSyncing] = useState(false);
const [msg, setMsg] = useState<string | null>(null);
const [msgType, setMsgType] = useState<"success" | "error" | null>(null);
// v1.12.1 — read off the consolidated /api/integrations/status
// envelope; the per-card /api/integrations/moodlog/status round-trip
// is gone. Mutations invalidate the envelope so the card's webhook
// secret / entry-count / last-sync repaint on the next refetch.
const status = viewModel;
const refreshStatus = () =>
queryClient.invalidateQueries({
queryKey: queryKeys.integrationsStatus(),
});
async function handleSave(e: React.FormEvent) {
e.preventDefault();
setSaving(true);
setMsg(null);
const res = await fetch("/api/settings/moodlog", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: url.trim(), apiKey: apiKey.trim() }),
});
if (res.ok) {
setMsg(t("settings.saved"));
setMsgType("success");
setUrl("");
setApiKey("");
void refreshStatus();
} else {
const json = await res.json();
setMsg(json.error || t("settings.savingError"));
setMsgType("error");
}
setSaving(false);
}
async function handleSync(fullSync = false) {
setSyncing(true);
setMsg(null);
try {
const res = await fetch("/api/integrations/moodlog/sync", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ fullSync }),
});
if (res.ok) {
const json = await res.json();
setMsg(
t("settings.moodLogSyncResult", { count: json.data.imported }),
);
setMsgType("success");
void refreshStatus();
} else {
setMsg(t("settings.moodLogSyncFailed"));
setMsgType("error");
}
} catch {
setMsg(t("settings.moodLogSyncFailed"));
setMsgType("error");
}
setSyncing(false);
}
async function handleDisconnect() {
const res = await fetch("/api/settings/moodlog", { method: "DELETE" });
if (res.ok) {
setMsg(t("settings.moodLogDisconnected"));
setMsgType("success");
void refreshStatus();
}
}
// Mirror Withings logic: the pill state comes from the cross-
// integration envelope, which also carries `legacyLastSyncedAt`
// (the user's moodLog last-sync timestamp) so a manual sync paints
// a fresh "X min ago" once the envelope refetches.
const pillState: IntegrationPillState = status?.configured
? pillStateFor(viewModel)
: "disconnected";
const pillLastSyncAt =
status?.legacyLastSyncedAt ?? viewModel?.lastSuccessAt ?? null;
const errorMessage =
pillState === "error" && viewModel?.lastError ? viewModel.lastError : null;
return (
<div className="bg-card border-border rounded-xl border p-6">
<SettingsCardHeader
icon={Smile}
title={
<a
href="https://moodlog.onback.io"
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
{t("settings.moodLogTitle")}
</a>
}
description={
<>
<p>{t("settings.moodLogDescription")}</p>
<p className="text-muted-foreground/80">
{t("settings.moodLogDeprecated")}
</p>
</>
}
status={
<IntegrationStatusPill state={pillState} lastSyncAt={pillLastSyncAt} />
}
/>
{/* v1.4.19 A5 — visual divider matches Withings for consistency
(the maintainer explicitly called the asymmetry out). */}
<hr
data-testid="integration-card-divider"
className="border-border/60 mt-4"
/>
<div className="mt-4 space-y-4">
{errorMessage && <IntegrationErrorMessage message={errorMessage} />}
<form onSubmit={handleSave} className="space-y-3">
<div>
<Label htmlFor="moodlog-url">{t("settings.moodLogUrl")}</Label>
<Input
id="moodlog-url"
type="url"
placeholder={t("settings.moodLogUrlPlaceholder")}
value={url}
onChange={(e) => setUrl(e.target.value)}
autoComplete="off"
spellCheck={false}
autoCapitalize="none"
enterKeyHint="next"
/>
</div>
<div>
<Label htmlFor="moodlog-api-key">
{t("settings.moodLogApiKey")}
</Label>
<PasswordInput
id="moodlog-api-key"
placeholder={
status?.configured
? t("settings.withingsCredentialsSavedPlaceholder")
: t("settings.moodLogApiKeyPlaceholder")
}
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
autoComplete="off"
inputMode="text"
spellCheck={false}
autoCapitalize="none"
enterKeyHint="done"
/>
</div>
{/* v1.4.33 — save-button placement contract:
right-aligned primary `Speichern` with the secondary
`Verbindung testen` immediately to its left. Mood Log
used to left-align its row, which was the only
integration form that did. */}
<div className="flex flex-wrap items-center justify-end gap-2">
<TestConnectionButton
endpoint="/api/integrations/moodlog/test"
disabled={!status?.configured}
/>
<Button
type="submit"
disabled={saving || (!url.trim() && !apiKey.trim())}
size="sm"
className="min-h-11"
>
{saving ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<Save className="mr-1 h-3.5 w-3.5" />
)}
{t("common.save")}
</Button>
</div>
</form>
{status?.configured && (
<div className="space-y-3 border-t pt-3">
{status.webhookSecret && (
<div>
<Label htmlFor="moodlog-webhook-secret">
{t("settings.moodLogWebhookSecret")}
</Label>
{/* v1.4.33 — the webhook secret is a long hex string;
on a 393 CSS px viewport the input + Copy button
used to push past the card edge and trigger a body
horizontal scroll. Stack vertically on `<sm` and
break the input value so it wraps inside its own
box. */}
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<Input
id="moodlog-webhook-secret"
value={status.webhookSecret}
readOnly
className="font-mono text-xs break-all"
/>
<Button
variant="outline"
size="sm"
onClick={() => {
navigator.clipboard.writeText(status.webhookSecret!);
setMsg(t("common.copied"));
setMsgType("success");
}}
className="min-h-11 w-full sm:w-auto"
>
{/* v1.4.22 D / D-DSGN-M-04 — use the proper
common.copy key instead of stripping the
exclamation off common.copied. */}
{t("common.copy")}
</Button>
</div>
<p className="text-muted-foreground mt-1 text-xs">
{t("settings.moodLogWebhookSecretHelp")}
</p>
</div>
)}
{/* v1.4.19 A5 — The "letzter Sync" line that used to
live here is gone. The pill in the card header
already carries that information; repeating it
inside the body is the redundancy the maintainer flagged. The
entry-count is the only number we still surface
because it's an integration-specific datapoint the
pill cannot convey. */}
<div className="flex flex-wrap gap-4 text-sm">
<span>
{t("settings.moodLogEntries")}: {status.entryCount}
</span>
</div>
{/* v1.4.33 — the Sync / Voll-Sync / Trennen triplet
overflowed the card edge on a 393 CSS px viewport
(~376 px of buttons + gaps inside 345 px of inner
width). Match the Withings card pattern: `flex-wrap`
+ `min-w-[10rem]` on `<sm` so the triggers stack two
per row and fill their column cleanly; tablet+ keeps
the inline row. */}
<div className="flex flex-wrap items-start gap-2 [&>*]:min-w-[10rem] sm:[&>*]:min-w-0">
<Button
variant="outline"
size="sm"
className="min-h-11"
disabled={syncing}
onClick={() => handleSync(false)}
>
{syncing ? (
<Loader2 className="mr-1 h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
) : (
<RefreshCw className="mr-1 h-3.5 w-3.5" />
)}
{t("settings.moodLogSync")}
</Button>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
variant="outline"
size="sm"
className="min-h-11"
disabled={syncing}
>
<RefreshCw className="mr-1 h-3.5 w-3.5" />
{t("settings.moodLogFullSync")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("settings.moodLogFullSyncTitle")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("settings.moodLogFullSyncDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={() => handleSync(true)}>
{t("settings.moodLogFullSyncConfirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
{/* v1.4.3: aligned with the Withings disconnect button —
outline + text-destructive instead of solid destructive
so the trigger reads as a reversible action; the actual
"yes, disconnect" confirmation inside the dialog keeps
its solid-red treatment. */}
<Button
variant="outline"
size="sm"
className="text-destructive min-h-11"
>
<Unlink className="mr-1 h-3.5 w-3.5" />
{t("settings.moodLogDisconnect")}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("settings.moodLogDisconnectTitle")}
</AlertDialogTitle>
<AlertDialogDescription>
{t("settings.moodLogDisconnectDescription")}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("common.cancel")}</AlertDialogCancel>
<AlertDialogAction onClick={handleDisconnect}>
{t("settings.moodLogDisconnect")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
)}
{msg && (
<p
role="alert"
className={`text-sm ${msgType === "error" ? "text-destructive" : "text-success"}`}
>
{msg}
</p>
)}
</div>
</div>
);
}