Files
Termix/src/ui/desktop/user/UserProfile.tsx
T

741 lines
28 KiB
TypeScript
Raw Normal View History

+7
2025-11-05 10:36:16 -06:00
import React, { useState, useEffect } from "react";
import { Label } from "@/components/ui/label.tsx";
import { Button } from "@/components/ui/button.tsx";
import { PasswordInput } from "@/components/ui/password-input.tsx";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert.tsx";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs.tsx";
import { Separator } from "@/components/ui/separator.tsx";
2025-11-17 09:46:05 -06:00
import { Switch } from "@/components/ui/switch.tsx";
+1
2025-12-31 22:20:12 -06:00
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select.tsx";
import {
User,
Shield,
AlertCircle,
Palette,
Sun,
Moon,
Monitor,
} from "lucide-react";
import { useTheme } from "@/components/theme-provider";
+7
2025-11-05 10:36:16 -06:00
import { TOTPSetup } from "@/ui/desktop/user/TOTPSetup.tsx";
import {
getUserInfo,
getVersionInfo,
deleteAccount,
logoutUser,
isElectron,
+1
2025-12-31 22:20:12 -06:00
getUserRoles,
type UserRole,
+7
2025-11-05 10:36:16 -06:00
} from "@/ui/main-axios.ts";
import { PasswordReset } from "@/ui/desktop/user/PasswordReset.tsx";
import { useTranslation } from "react-i18next";
import { LanguageSwitcher } from "@/ui/desktop/user/LanguageSwitcher.tsx";
import { useSidebar } from "@/components/ui/sidebar.tsx";
interface UserProfileProps {
isTopbarOpen?: boolean;
2025-11-17 09:46:05 -06:00
rightSidebarOpen?: boolean;
rightSidebarWidth?: number;
+7
2025-11-05 10:36:16 -06:00
}
async function handleLogout() {
try {
await logoutUser();
if (isElectron()) {
localStorage.removeItem("jwt");
const configuredServerUrl = (
window as Window &
typeof globalThis & {
configuredServerUrl?: string;
}
).configuredServerUrl;
if (configuredServerUrl) {
const iframe = document.querySelector("iframe");
if (iframe && iframe.contentWindow) {
try {
const serverOrigin = new URL(configuredServerUrl).origin;
iframe.contentWindow.postMessage(
{
type: "CLEAR_AUTH_DATA",
timestamp: Date.now(),
},
serverOrigin,
);
2025-11-17 09:46:05 -06:00
} catch (err) {
console.error("User profile operation failed:", err);
}
+7
2025-11-05 10:36:16 -06:00
}
}
}
window.location.reload();
} catch (error) {
console.error("Logout failed:", error);
window.location.reload();
}
}
2025-11-17 09:46:05 -06:00
export function UserProfile({
isTopbarOpen = true,
rightSidebarOpen = false,
rightSidebarWidth = 400,
}: UserProfileProps) {
+7
2025-11-05 10:36:16 -06:00
const { t } = useTranslation();
const { state: sidebarState } = useSidebar();
+1
2025-12-31 22:20:12 -06:00
const { theme, setTheme } = useTheme();
+7
2025-11-05 10:36:16 -06:00
const [userInfo, setUserInfo] = useState<{
username: string;
is_admin: boolean;
is_oidc: boolean;
2025-11-17 09:46:05 -06:00
is_dual_auth: boolean;
+7
2025-11-05 10:36:16 -06:00
totp_enabled: boolean;
} | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [versionInfo, setVersionInfo] = useState<{ version: string } | null>(
null,
);
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
const [deletePassword, setDeletePassword] = useState("");
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
2025-11-17 09:46:05 -06:00
const [fileColorCoding, setFileColorCoding] = useState<boolean>(
localStorage.getItem("fileColorCoding") !== "false",
);
const [commandAutocomplete, setCommandAutocomplete] = useState<boolean>(
+1
2025-12-31 22:20:12 -06:00
localStorage.getItem("commandAutocomplete") === "true",
2025-11-17 09:46:05 -06:00
);
+1
2025-12-31 22:20:12 -06:00
const [terminalSyntaxHighlighting, setTerminalSyntaxHighlighting] =
useState<boolean>(
() => localStorage.getItem("terminalSyntaxHighlighting") === "true",
);
const [defaultSnippetFoldersCollapsed, setDefaultSnippetFoldersCollapsed] =
useState<boolean>(
localStorage.getItem("defaultSnippetFoldersCollapsed") !== "false",
);
const [showHostTags, setShowHostTags] = useState<boolean>(() => {
const saved = localStorage.getItem("showHostTags");
return saved !== null ? saved === "true" : true;
});
const [userRoles, setUserRoles] = useState<UserRole[]>([]);
+7
2025-11-05 10:36:16 -06:00
useEffect(() => {
fetchUserInfo();
fetchVersion();
}, []);
const fetchVersion = async () => {
try {
const info = await getVersionInfo();
setVersionInfo({ version: info.localVersion });
} catch {
const { toast } = await import("sonner");
toast.error(t("user.failedToLoadVersionInfo"));
}
};
const fetchUserInfo = async () => {
setLoading(true);
setError(null);
try {
const info = await getUserInfo();
setUserInfo({
username: info.username,
is_admin: info.is_admin,
is_oidc: info.is_oidc,
2025-11-17 09:46:05 -06:00
is_dual_auth: info.is_dual_auth || false,
+7
2025-11-05 10:36:16 -06:00
totp_enabled: info.totp_enabled || false,
});
+1
2025-12-31 22:20:12 -06:00
try {
const rolesResponse = await getUserRoles(info.userId);
setUserRoles(rolesResponse.roles || []);
} catch (rolesErr) {
console.error("Failed to fetch user roles:", rolesErr);
setUserRoles([]);
}
+7
2025-11-05 10:36:16 -06:00
} catch (err: unknown) {
const error = err as { response?: { data?: { error?: string } } };
setError(error?.response?.data?.error || t("errors.loadFailed"));
} finally {
setLoading(false);
}
};
const handleTOTPStatusChange = (enabled: boolean) => {
if (userInfo) {
setUserInfo({ ...userInfo, totp_enabled: enabled });
}
};
2025-11-17 09:46:05 -06:00
const handleFileColorCodingToggle = (enabled: boolean) => {
setFileColorCoding(enabled);
localStorage.setItem("fileColorCoding", enabled.toString());
window.dispatchEvent(new Event("fileColorCodingChanged"));
};
const handleCommandAutocompleteToggle = (enabled: boolean) => {
setCommandAutocomplete(enabled);
localStorage.setItem("commandAutocomplete", enabled.toString());
};
+1
2025-12-31 22:20:12 -06:00
const handleTerminalSyntaxHighlightingToggle = (enabled: boolean) => {
setTerminalSyntaxHighlighting(enabled);
localStorage.setItem("terminalSyntaxHighlighting", enabled.toString());
window.dispatchEvent(new Event("terminalSyntaxHighlightingChanged"));
};
const handleDefaultSnippetFoldersCollapsedToggle = (enabled: boolean) => {
setDefaultSnippetFoldersCollapsed(enabled);
localStorage.setItem("defaultSnippetFoldersCollapsed", enabled.toString());
window.dispatchEvent(new Event("defaultSnippetFoldersCollapsedChanged"));
};
const handleShowHostTagsToggle = (enabled: boolean) => {
setShowHostTags(enabled);
localStorage.setItem("showHostTags", enabled.toString());
window.dispatchEvent(new Event("showHostTagsChanged"));
};
+7
2025-11-05 10:36:16 -06:00
const handleDeleteAccount = async (e: React.FormEvent) => {
e.preventDefault();
setDeleteLoading(true);
setDeleteError(null);
if (!deletePassword.trim()) {
setDeleteError(t("leftSidebar.passwordRequired"));
setDeleteLoading(false);
return;
}
try {
await deleteAccount(deletePassword);
handleLogout();
} catch (err: unknown) {
setDeleteError(
(err as { response?: { data?: { error?: string } } })?.response?.data
?.error || t("leftSidebar.failedToDeleteAccount"),
);
setDeleteLoading(false);
}
};
const topMarginPx = isTopbarOpen ? 74 : 26;
const leftMarginPx = sidebarState === "collapsed" ? 26 : 8;
const bottomMarginPx = 8;
const wrapperStyle: React.CSSProperties = {
marginLeft: leftMarginPx,
2025-11-17 09:46:05 -06:00
marginRight: rightSidebarOpen
? `calc(var(--right-sidebar-width, ${rightSidebarWidth}px) + 8px)`
: 17,
+7
2025-11-05 10:36:16 -06:00
marginTop: topMarginPx,
marginBottom: bottomMarginPx,
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
2025-11-17 09:46:05 -06:00
transition:
"margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear",
+7
2025-11-05 10:36:16 -06:00
};
if (loading) {
return (
<div
style={wrapperStyle}
+1
2025-12-31 22:20:12 -06:00
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
+7
2025-11-05 10:36:16 -06:00
>
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between px-3 pt-2 pb-2">
<h1 className="font-bold text-lg">{t("nav.userProfile")}</h1>
</div>
<Separator className="p-0.25 w-full" />
<div className="flex-1 flex items-center justify-center">
+1
2025-12-31 22:20:12 -06:00
<div className="animate-pulse text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("common.loading")}
</div>
</div>
</div>
</div>
);
}
if (error || !userInfo) {
return (
<div
style={wrapperStyle}
+1
2025-12-31 22:20:12 -06:00
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
+7
2025-11-05 10:36:16 -06:00
>
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between px-3 pt-2 pb-2">
<h1 className="font-bold text-lg">{t("nav.userProfile")}</h1>
</div>
<Separator className="p-0.25 w-full" />
<div className="flex-1 flex items-center justify-center p-6">
<Alert
variant="destructive"
className="bg-red-900/20 border-red-500/50"
>
<AlertCircle className="h-4 w-4" />
<AlertTitle className="text-red-400">
{t("common.error")}
</AlertTitle>
<AlertDescription className="text-red-300">
{error || t("errors.loadFailed")}
</AlertDescription>
</Alert>
</div>
</div>
</div>
);
}
return (
<>
<div
style={wrapperStyle}
+1
2025-12-31 22:20:12 -06:00
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
+7
2025-11-05 10:36:16 -06:00
>
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between px-3 pt-2 pb-2">
<h1 className="font-bold text-lg">{t("nav.userProfile")}</h1>
</div>
<Separator className="p-0.25 w-full" />
+1
2025-12-31 22:20:12 -06:00
<div className="px-6 py-4 overflow-auto thin-scrollbar flex-1">
+7
2025-11-05 10:36:16 -06:00
<Tabs defaultValue="profile" className="w-full">
+1
2025-12-31 22:20:12 -06:00
<TabsList className="mb-4 bg-elevated border-2 border-edge">
+7
2025-11-05 10:36:16 -06:00
<TabsTrigger
value="profile"
+1
2025-12-31 22:20:12 -06:00
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
+7
2025-11-05 10:36:16 -06:00
>
<User className="w-4 h-4" />
+1
2025-12-31 22:20:12 -06:00
{t("profile.account")}
</TabsTrigger>
<TabsTrigger
value="appearance"
className="flex items-center gap-2 data-[state=active]:bg-button"
>
<Palette className="w-4 h-4" />
{t("profile.appearance")}
+7
2025-11-05 10:36:16 -06:00
</TabsTrigger>
2025-11-17 09:46:05 -06:00
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
+7
2025-11-05 10:36:16 -06:00
<TabsTrigger
value="security"
+1
2025-12-31 22:20:12 -06:00
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
+7
2025-11-05 10:36:16 -06:00
>
<Shield className="w-4 h-4" />
{t("profile.security")}
</TabsTrigger>
)}
</TabsList>
<TabsContent value="profile" className="space-y-4">
+1
2025-12-31 22:20:12 -06:00
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
+7
2025-11-05 10:36:16 -06:00
<h3 className="text-lg font-semibold mb-4">
{t("profile.accountInfo")}
</h3>
<div className="grid grid-cols-2 gap-4">
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("common.username")}
</Label>
+1
2025-12-31 22:20:12 -06:00
<p className="text-lg font-medium mt-1 text-foreground">
+7
2025-11-05 10:36:16 -06:00
{userInfo.username}
</p>
</div>
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("profile.role")}
</Label>
+1
2025-12-31 22:20:12 -06:00
<div className="mt-1">
{userRoles.length > 0 ? (
<div className="flex flex-wrap gap-2">
{userRoles.map((role) => (
<span
key={role.roleId}
className="inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium bg-muted/50 text-foreground border border-border"
>
{t(role.roleDisplayName)}
</span>
))}
</div>
) : (
<p className="text-lg font-medium text-foreground">
{userInfo.is_admin
? t("interface.administrator")
: t("interface.user")}
</p>
)}
</div>
+7
2025-11-05 10:36:16 -06:00
</div>
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("profile.authMethod")}
</Label>
+1
2025-12-31 22:20:12 -06:00
<p className="text-lg font-medium mt-1 text-foreground">
2025-11-17 09:46:05 -06:00
{userInfo.is_dual_auth
? t("profile.externalAndLocal")
: userInfo.is_oidc
? t("profile.external")
: t("profile.local")}
+7
2025-11-05 10:36:16 -06:00
</p>
</div>
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("profile.twoFactorAuth")}
</Label>
<p className="text-lg font-medium mt-1">
2025-11-17 09:46:05 -06:00
{userInfo.is_oidc && !userInfo.is_dual_auth ? (
+1
2025-12-31 22:20:12 -06:00
<span className="text-muted-foreground">
+7
2025-11-05 10:36:16 -06:00
{t("auth.lockedOidcAuth")}
</span>
) : userInfo.totp_enabled ? (
<span className="text-green-400 flex items-center gap-1">
<Shield className="w-4 h-4" />
{t("common.enabled")}
</span>
) : (
+1
2025-12-31 22:20:12 -06:00
<span className="text-muted-foreground">
+7
2025-11-05 10:36:16 -06:00
{t("common.disabled")}
</span>
)}
</p>
</div>
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("common.version")}
</Label>
+1
2025-12-31 22:20:12 -06:00
<p className="text-lg font-medium mt-1 text-foreground">
+7
2025-11-05 10:36:16 -06:00
{versionInfo?.version || t("common.loading")}
</p>
</div>
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="mt-6 pt-6 border-t border-edge">
+7
2025-11-05 10:36:16 -06:00
<div className="flex items-center justify-between">
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-red-400">
{t("leftSidebar.deleteAccount")}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{t("leftSidebar.deleteAccountWarningShort")}
</p>
</div>
<Button
variant="destructive"
onClick={() => setDeleteAccountOpen(true)}
>
{t("leftSidebar.deleteAccount")}
</Button>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="appearance" className="space-y-4">
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
<h3 className="text-lg font-semibold mb-4">
{t("profile.languageLocalization")}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label className="text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("common.language")}
</Label>
+1
2025-12-31 22:20:12 -06:00
<p className="text-sm text-muted-foreground mt-1">
+7
2025-11-05 10:36:16 -06:00
{t("profile.selectPreferredLanguage")}
</p>
</div>
<LanguageSwitcher />
</div>
</div>
+1
2025-12-31 22:20:12 -06:00
</div>
+7
2025-11-05 10:36:16 -06:00
+1
2025-12-31 22:20:12 -06:00
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
<h3 className="text-lg font-semibold mb-4">
{t("profile.appearance")}
</h3>
<div className="space-y-4">
2025-11-17 09:46:05 -06:00
<div className="flex items-center justify-between">
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
{t("profile.theme")}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{t("profile.appearanceDesc")}
</p>
</div>
<Select value={theme} onValueChange={setTheme}>
<SelectTrigger className="w-[140px]">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="light">
<div className="flex items-center gap-2">
<Sun className="w-4 h-4" />
{t("profile.themeLight")}
</div>
</SelectItem>
<SelectItem value="dark">
<div className="flex items-center gap-2">
<Moon className="w-4 h-4" />
{t("profile.themeDark")}
</div>
</SelectItem>
<SelectItem value="system">
<div className="flex items-center gap-2">
<Monitor className="w-4 h-4" />
{t("profile.themeSystem")}
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
<h3 className="text-lg font-semibold mb-4">
{t("profile.fileManagerSettings")}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label className="text-foreground-secondary">
2025-11-17 09:46:05 -06:00
{t("profile.fileColorCoding")}
</Label>
+1
2025-12-31 22:20:12 -06:00
<p className="text-sm text-muted-foreground mt-1">
2025-11-17 09:46:05 -06:00
{t("profile.fileColorCodingDesc")}
</p>
</div>
<Switch
checked={fileColorCoding}
onCheckedChange={handleFileColorCodingToggle}
/>
</div>
</div>
+1
2025-12-31 22:20:12 -06:00
</div>
2025-11-17 09:46:05 -06:00
+1
2025-12-31 22:20:12 -06:00
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
<h3 className="text-lg font-semibold mb-4">
{t("profile.terminalSettings")}
</h3>
<div className="space-y-4">
2025-11-17 09:46:05 -06:00
<div className="flex items-center justify-between">
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
2025-11-17 09:46:05 -06:00
{t("profile.commandAutocomplete")}
</Label>
+1
2025-12-31 22:20:12 -06:00
<p className="text-sm text-muted-foreground mt-1">
2025-11-17 09:46:05 -06:00
{t("profile.commandAutocompleteDesc")}
</p>
</div>
<Switch
checked={commandAutocomplete}
onCheckedChange={handleCommandAutocompleteToggle}
/>
</div>
+7
2025-11-05 10:36:16 -06:00
<div className="flex items-center justify-between">
<div>
+1
2025-12-31 22:20:12 -06:00
<Label className="text-foreground-secondary">
{t("profile.terminalSyntaxHighlighting")}{" "}
<span className="text-xs text-yellow-500 font-semibold">
(BETA)
</span>
+7
2025-11-05 10:36:16 -06:00
</Label>
+1
2025-12-31 22:20:12 -06:00
<p className="text-sm text-muted-foreground mt-1">
{t("profile.terminalSyntaxHighlightingDesc")}
+7
2025-11-05 10:36:16 -06:00
</p>
</div>
+1
2025-12-31 22:20:12 -06:00
<Switch
checked={terminalSyntaxHighlighting}
onCheckedChange={handleTerminalSyntaxHighlightingToggle}
/>
</div>
</div>
</div>
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
<h3 className="text-lg font-semibold mb-4">
{t("profile.hostSidebarSettings")}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label className="text-foreground-secondary">
{t("profile.showHostTags")}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{t("profile.showHostTagsDesc")}
</p>
</div>
<Switch
checked={showHostTags}
onCheckedChange={handleShowHostTagsToggle}
/>
</div>
</div>
</div>
<div className="rounded-lg border-2 border-edge bg-elevated p-4">
<h3 className="text-lg font-semibold mb-4">
{t("profile.snippetsSettings")}
</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label className="text-foreground-secondary">
{t("profile.defaultSnippetFoldersCollapsed")}
</Label>
<p className="text-sm text-muted-foreground mt-1">
{t("profile.defaultSnippetFoldersCollapsedDesc")}
</p>
</div>
<Switch
checked={defaultSnippetFoldersCollapsed}
onCheckedChange={
handleDefaultSnippetFoldersCollapsedToggle
}
/>
+7
2025-11-05 10:36:16 -06:00
</div>
</div>
</div>
</TabsContent>
<TabsContent value="security" className="space-y-4">
<TOTPSetup
isEnabled={userInfo.totp_enabled}
onStatusChange={handleTOTPStatusChange}
/>
2025-11-17 09:46:05 -06:00
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
<PasswordReset userInfo={userInfo} />
)}
+7
2025-11-05 10:36:16 -06:00
</TabsContent>
</Tabs>
</div>
</div>
</div>
{deleteAccountOpen && (
<div
className="fixed top-0 left-0 right-0 bottom-0 z-[999999] pointer-events-auto isolate"
style={{
transform: "translateZ(0)",
willChange: "z-index",
}}
>
<div
+1
2025-12-31 22:20:12 -06:00
className="w-[400px] h-full bg-canvas border-r-2 border-edge flex flex-col shadow-2xl relative isolate z-[9999999]"
+7
2025-11-05 10:36:16 -06:00
style={{
boxShadow: "4px 0 20px rgba(0, 0, 0, 0.5)",
transform: "translateZ(0)",
}}
onClick={(e) => e.stopPropagation()}
>
+1
2025-12-31 22:20:12 -06:00
<div className="flex items-center justify-between p-4 border-b border-edge">
<h2 className="text-lg font-semibold text-foreground">
+7
2025-11-05 10:36:16 -06:00
{t("leftSidebar.deleteAccount")}
</h2>
<Button
variant="outline"
size="sm"
onClick={() => {
setDeleteAccountOpen(false);
setDeletePassword("");
setDeleteError(null);
}}
+1
2025-12-31 22:20:12 -06:00
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-foreground transition-colors flex items-center justify-center"
+7
2025-11-05 10:36:16 -06:00
title={t("leftSidebar.closeDeleteAccount")}
>
<span className="text-lg font-bold leading-none">×</span>
</Button>
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="flex-1 overflow-y-auto p-4 thin-scrollbar">
+7
2025-11-05 10:36:16 -06:00
<div className="space-y-4">
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-secondary">
+7
2025-11-05 10:36:16 -06:00
{t("leftSidebar.deleteAccountWarning")}
2025-11-17 09:46:05 -06:00
<Alert variant="destructive" className="mb-5 mt-5">
+7
2025-11-05 10:36:16 -06:00
<AlertTitle>{t("common.warning")}</AlertTitle>
<AlertDescription>
{t("leftSidebar.deleteAccountWarningDetails")}
</AlertDescription>
</Alert>
{deleteError && (
<Alert variant="destructive">
<AlertTitle>{t("common.error")}</AlertTitle>
<AlertDescription>{deleteError}</AlertDescription>
</Alert>
)}
<form onSubmit={handleDeleteAccount} className="space-y-4">
<div className="space-y-2">
<Label htmlFor="delete-password">
{t("leftSidebar.confirmPassword")}
</Label>
<PasswordInput
id="delete-password"
value={deletePassword}
onChange={(e) => setDeletePassword(e.target.value)}
placeholder={t("placeholders.confirmPassword")}
required
/>
</div>
<div className="flex gap-2">
<Button
type="submit"
variant="destructive"
className="flex-1"
disabled={deleteLoading || !deletePassword.trim()}
>
{deleteLoading
? t("leftSidebar.deleting")
: t("leftSidebar.deleteAccount")}
</Button>
<Button
type="button"
variant="outline"
onClick={() => {
setDeleteAccountOpen(false);
setDeletePassword("");
setDeleteError(null);
}}
>
{t("leftSidebar.cancel")}
</Button>
</div>
</form>
</div>
</div>
</div>
<div
className="flex-1 cursor-pointer"
onClick={() => {
setDeleteAccountOpen(false);
setDeletePassword("");
setDeleteError(null);
}}
/>
</div>
</div>
)}
</>
);
}