- Convert all dynamic imports of main-axios.ts to static imports (10 files) - Convert all dynamic imports of sonner to static imports (4 files) - Add manual chunking configuration to vite.config.ts for better bundle splitting - react-vendor: React and React DOM - ui-vendor: Radix UI, lucide-react, clsx, tailwind-merge - monaco: Monaco Editor - codemirror: CodeMirror and related packages - Increase chunkSizeWarningLimit to 1000kB This resolves Vite warnings about mixed import strategies preventing proper code-splitting. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
741 lines
28 KiB
TypeScript
741 lines
28 KiB
TypeScript
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";
|
||
import { Switch } from "@/components/ui/switch.tsx";
|
||
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";
|
||
import { TOTPSetup } from "@/ui/desktop/user/TOTPSetup.tsx";
|
||
import {
|
||
getUserInfo,
|
||
getVersionInfo,
|
||
deleteAccount,
|
||
logoutUser,
|
||
isElectron,
|
||
getUserRoles,
|
||
type UserRole,
|
||
} 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";
|
||
import { toast } from "sonner";
|
||
|
||
interface UserProfileProps {
|
||
isTopbarOpen?: boolean;
|
||
rightSidebarOpen?: boolean;
|
||
rightSidebarWidth?: number;
|
||
}
|
||
|
||
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,
|
||
);
|
||
} catch (err) {
|
||
console.error("User profile operation failed:", err);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
window.location.reload();
|
||
} catch (error) {
|
||
console.error("Logout failed:", error);
|
||
window.location.reload();
|
||
}
|
||
}
|
||
|
||
export function UserProfile({
|
||
isTopbarOpen = true,
|
||
rightSidebarOpen = false,
|
||
rightSidebarWidth = 400,
|
||
}: UserProfileProps) {
|
||
const { t } = useTranslation();
|
||
const { state: sidebarState } = useSidebar();
|
||
const { theme, setTheme } = useTheme();
|
||
const [userInfo, setUserInfo] = useState<{
|
||
username: string;
|
||
is_admin: boolean;
|
||
is_oidc: boolean;
|
||
is_dual_auth: boolean;
|
||
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);
|
||
const [fileColorCoding, setFileColorCoding] = useState<boolean>(
|
||
localStorage.getItem("fileColorCoding") !== "false",
|
||
);
|
||
const [commandAutocomplete, setCommandAutocomplete] = useState<boolean>(
|
||
localStorage.getItem("commandAutocomplete") === "true",
|
||
);
|
||
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[]>([]);
|
||
|
||
useEffect(() => {
|
||
fetchUserInfo();
|
||
fetchVersion();
|
||
}, []);
|
||
|
||
const fetchVersion = async () => {
|
||
try {
|
||
const info = await getVersionInfo();
|
||
setVersionInfo({ version: info.localVersion });
|
||
} catch {
|
||
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,
|
||
is_dual_auth: info.is_dual_auth || false,
|
||
totp_enabled: info.totp_enabled || false,
|
||
});
|
||
|
||
try {
|
||
const rolesResponse = await getUserRoles(info.userId);
|
||
setUserRoles(rolesResponse.roles || []);
|
||
} catch (rolesErr) {
|
||
console.error("Failed to fetch user roles:", rolesErr);
|
||
setUserRoles([]);
|
||
}
|
||
} 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 });
|
||
}
|
||
};
|
||
|
||
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());
|
||
};
|
||
|
||
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"));
|
||
};
|
||
|
||
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,
|
||
marginRight: rightSidebarOpen
|
||
? `calc(var(--right-sidebar-width, ${rightSidebarWidth}px) + 8px)`
|
||
: 17,
|
||
marginTop: topMarginPx,
|
||
marginBottom: bottomMarginPx,
|
||
height: `calc(100vh - ${topMarginPx + bottomMarginPx}px)`,
|
||
transition:
|
||
"margin-left 200ms linear, margin-right 200ms linear, margin-top 200ms linear",
|
||
};
|
||
|
||
if (loading) {
|
||
return (
|
||
<div
|
||
style={wrapperStyle}
|
||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||
>
|
||
<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">
|
||
<div className="animate-pulse text-foreground-secondary">
|
||
{t("common.loading")}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (error || !userInfo) {
|
||
return (
|
||
<div
|
||
style={wrapperStyle}
|
||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||
>
|
||
<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}
|
||
className="bg-canvas text-foreground rounded-lg border-2 border-edge overflow-hidden"
|
||
>
|
||
<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="px-6 py-4 overflow-auto thin-scrollbar flex-1">
|
||
<Tabs defaultValue="profile" className="w-full">
|
||
<TabsList className="mb-4 bg-elevated border-2 border-edge">
|
||
<TabsTrigger
|
||
value="profile"
|
||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||
>
|
||
<User className="w-4 h-4" />
|
||
{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")}
|
||
</TabsTrigger>
|
||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||
<TabsTrigger
|
||
value="security"
|
||
className="flex items-center gap-2 bg-elevated data-[state=active]:bg-button data-[state=active]:border data-[state=active]:border-edge"
|
||
>
|
||
<Shield className="w-4 h-4" />
|
||
{t("profile.security")}
|
||
</TabsTrigger>
|
||
)}
|
||
</TabsList>
|
||
|
||
<TabsContent value="profile" 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.accountInfo")}
|
||
</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("common.username")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1 text-foreground">
|
||
{userInfo.username}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.role")}
|
||
</Label>
|
||
<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>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.authMethod")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1 text-foreground">
|
||
{userInfo.is_dual_auth
|
||
? t("profile.externalAndLocal")
|
||
: userInfo.is_oidc
|
||
? t("profile.external")
|
||
: t("profile.local")}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.twoFactorAuth")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1">
|
||
{userInfo.is_oidc && !userInfo.is_dual_auth ? (
|
||
<span className="text-muted-foreground">
|
||
{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>
|
||
) : (
|
||
<span className="text-muted-foreground">
|
||
{t("common.disabled")}
|
||
</span>
|
||
)}
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("common.version")}
|
||
</Label>
|
||
<p className="text-lg font-medium mt-1 text-foreground">
|
||
{versionInfo?.version || t("common.loading")}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mt-6 pt-6 border-t border-edge">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<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">
|
||
{t("common.language")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.selectPreferredLanguage")}
|
||
</p>
|
||
</div>
|
||
<LanguageSwitcher />
|
||
</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.appearance")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<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">
|
||
{t("profile.fileColorCoding")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.fileColorCodingDesc")}
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
checked={fileColorCoding}
|
||
onCheckedChange={handleFileColorCodingToggle}
|
||
/>
|
||
</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.terminalSettings")}
|
||
</h3>
|
||
<div className="space-y-4">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.commandAutocomplete")}
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.commandAutocompleteDesc")}
|
||
</p>
|
||
</div>
|
||
<Switch
|
||
checked={commandAutocomplete}
|
||
onCheckedChange={handleCommandAutocompleteToggle}
|
||
/>
|
||
</div>
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<Label className="text-foreground-secondary">
|
||
{t("profile.terminalSyntaxHighlighting")}{" "}
|
||
<span className="text-xs text-yellow-500 font-semibold">
|
||
(BETA)
|
||
</span>
|
||
</Label>
|
||
<p className="text-sm text-muted-foreground mt-1">
|
||
{t("profile.terminalSyntaxHighlightingDesc")}
|
||
</p>
|
||
</div>
|
||
<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
|
||
}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</TabsContent>
|
||
|
||
<TabsContent value="security" className="space-y-4">
|
||
<TOTPSetup
|
||
isEnabled={userInfo.totp_enabled}
|
||
onStatusChange={handleTOTPStatusChange}
|
||
/>
|
||
|
||
{(!userInfo.is_oidc || userInfo.is_dual_auth) && (
|
||
<PasswordReset userInfo={userInfo} />
|
||
)}
|
||
</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
|
||
className="w-[400px] h-full bg-canvas border-r-2 border-edge flex flex-col shadow-2xl relative isolate z-[9999999]"
|
||
style={{
|
||
boxShadow: "4px 0 20px rgba(0, 0, 0, 0.5)",
|
||
transform: "translateZ(0)",
|
||
}}
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex items-center justify-between p-4 border-b border-edge">
|
||
<h2 className="text-lg font-semibold text-foreground">
|
||
{t("leftSidebar.deleteAccount")}
|
||
</h2>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => {
|
||
setDeleteAccountOpen(false);
|
||
setDeletePassword("");
|
||
setDeleteError(null);
|
||
}}
|
||
className="h-8 w-8 p-0 hover:bg-red-500 hover:text-foreground transition-colors flex items-center justify-center"
|
||
title={t("leftSidebar.closeDeleteAccount")}
|
||
>
|
||
<span className="text-lg font-bold leading-none">×</span>
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="flex-1 overflow-y-auto p-4 thin-scrollbar">
|
||
<div className="space-y-4">
|
||
<div className="text-sm text-foreground-secondary">
|
||
{t("leftSidebar.deleteAccountWarning")}
|
||
<Alert variant="destructive" className="mb-5 mt-5">
|
||
<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>
|
||
)}
|
||
</>
|
||
);
|
||
}
|