summaryrefslogblamecommitdiff
path: root/makima/frontend/src/routes/settings.tsx
blob: 73537bd8006966cf2aca16b1621bb0c38e721991 (plain) (tree)
1
2
3
4
5
6
7
8
9
10
11
12











                                                            

                          

                            
                              




























































































































































































































































































                                                                                                                                                                                                                    





                                                                                

                   
                      

         












                                                                              























                                                                                                    



























































































































































































































































                                                                                                                                                                             
                                
                                                                                         
                                                    
                                                                       
                                                            
                  


                                                                                                   
                      




















































































                                                                                                                                    














































































































































                                                                                                                                
import { useState, useEffect, type FormEvent } from "react";
import { useAuth } from "../contexts/AuthContext";
import { useNavigate } from "react-router";
import { Masthead } from "../components/Masthead";
import {
  getApiKey,
  createApiKey,
  refreshApiKey,
  revokeApiKey,
  changePassword,
  changeEmail,
  deleteAccount,
  listRepositoryHistory,
  deleteRepositoryHistory,
  type ApiKeyInfo,
  type CreateApiKeyResponse,
  type RepositoryHistoryEntry,
} from "../lib/api";

// =============================================================================
// Password Strength Indicator
// =============================================================================

interface PasswordStrength {
  score: number;
  label: string;
  color: string;
  requirements: { met: boolean; text: string }[];
}

function getPasswordStrength(password: string): PasswordStrength {
  const requirements = [
    { met: password.length >= 6, text: "At least 6 characters" },
  ];

  const score = requirements.filter((r) => r.met).length;

  const label = score === 1 ? "Valid" : "Too short";
  const color = score === 1 ? "bg-green-500" : "bg-red-500";

  return { score, label, color, requirements };
}

// =============================================================================
// Confirmation Dialog Component
// =============================================================================

interface ConfirmDialogProps {
  isOpen: boolean;
  title: string;
  message: string;
  confirmText: string;
  confirmButtonClass?: string;
  requireInput?: string;
  inputPlaceholder?: string;
  onConfirm: () => void;
  onCancel: () => void;
}

function ConfirmDialog({
  isOpen,
  title,
  message,
  confirmText,
  confirmButtonClass = "bg-red-900/50 border-red-700 hover:bg-red-800/50",
  requireInput,
  inputPlaceholder,
  onConfirm,
  onCancel,
}: ConfirmDialogProps) {
  const [inputValue, setInputValue] = useState("");

  useEffect(() => {
    if (!isOpen) {
      setInputValue("");
    }
  }, [isOpen]);

  if (!isOpen) return null;

  const canConfirm = !requireInput || inputValue === requireInput;

  return (
    <div className="fixed inset-0 bg-black/70 flex items-center justify-center z-50">
      <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6 max-w-md w-full mx-4">
        <h3 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff] mb-3">{title}</h3>
        <p className="text-[#75aafc] text-xs font-mono mb-4">{message}</p>
        {requireInput && (
          <div className="mb-4">
            <label className="block text-xs font-mono text-[#8899aa] mb-2">
              Type <span className="text-[#9bc3ff]">{requireInput}</span> to confirm:
            </label>
            <input
              type="text"
              value={inputValue}
              onChange={(e) => setInputValue(e.target.value)}
              placeholder={inputPlaceholder}
              className="w-full px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
            />
          </div>
        )}
        <div className="flex gap-3 justify-end">
          <button
            onClick={onCancel}
            className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors"
          >
            Cancel
          </button>
          <button
            onClick={onConfirm}
            disabled={!canConfirm}
            className={`px-4 py-2 border font-mono text-xs uppercase tracking-wide transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${confirmButtonClass}`}
          >
            {confirmText}
          </button>
        </div>
      </div>
    </div>
  );
}

// =============================================================================
// Section Header Component
// =============================================================================

function SectionHeader({ children }: { children: React.ReactNode }) {
  return (
    <h2 className="text-[11px] font-mono uppercase tracking-wide text-[#8899aa] mb-3 pb-2 border-b border-[rgba(117,170,252,0.15)]">
      {children}
    </h2>
  );
}

// =============================================================================
// Form Input Component
// =============================================================================

function FormInput({
  label,
  type = "text",
  value,
  onChange,
  placeholder,
  required,
  disabled,
}: {
  label: string;
  type?: string;
  value: string;
  onChange: (value: string) => void;
  placeholder?: string;
  required?: boolean;
  disabled?: boolean;
}) {
  return (
    <div>
      <label className="block text-[10px] font-mono uppercase tracking-wide text-[#7788aa] mb-1">
        {label}
      </label>
      <input
        type={type}
        value={value}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        required={required}
        disabled={disabled}
        className="w-full px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3] disabled:opacity-50"
      />
    </div>
  );
}

// =============================================================================
// Alert Components
// =============================================================================

function ErrorAlert({ children }: { children: React.ReactNode }) {
  return (
    <div className="border border-red-700/50 bg-red-900/20 text-red-400 px-3 py-2 mb-4 font-mono text-xs">
      {children}
    </div>
  );
}

function SuccessAlert({ children }: { children: React.ReactNode }) {
  return (
    <div className="border border-green-700/50 bg-green-900/20 text-green-400 px-3 py-2 mb-4 font-mono text-xs">
      {children}
    </div>
  );
}

// =============================================================================
// Button Components
// =============================================================================

function PrimaryButton({
  children,
  onClick,
  disabled,
  type = "button",
}: {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
  type?: "button" | "submit";
}) {
  return (
    <button
      type={type}
      onClick={onClick}
      disabled={disabled}
      className="px-4 py-2 bg-[#3f6fb3] border border-[#75aafc] text-white font-mono text-xs uppercase tracking-wide hover:bg-[#4a7fc3] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
    >
      {children}
    </button>
  );
}

function SecondaryButton({
  children,
  onClick,
  disabled,
}: {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
      className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
    >
      {children}
    </button>
  );
}

function DangerButton({
  children,
  onClick,
  disabled,
}: {
  children: React.ReactNode;
  onClick?: () => void;
  disabled?: boolean;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      disabled={disabled}
      className="px-4 py-2 bg-red-900/30 border border-red-700/50 text-red-400 font-mono text-xs uppercase tracking-wide hover:bg-red-800/30 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
    >
      {children}
    </button>
  );
}

// =============================================================================
// Main Settings Page
// =============================================================================

export default function SettingsPage() {
  const { user, isAuthConfigured, signOut } = useAuth();
  const navigate = useNavigate();

  // API Key state
  const [apiKeyInfo, setApiKeyInfo] = useState<ApiKeyInfo | null>(null);
  const [newKey, setNewKey] = useState<string | null>(null);
  const [loading, setLoading] = useState(true);
  const [actionLoading, setActionLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [copied, setCopied] = useState(false);

  // Password change state
  const [passwordForm, setPasswordForm] = useState({
    currentPassword: "",
    newPassword: "",
    confirmPassword: "",
  });
  const [passwordLoading, setPasswordLoading] = useState(false);
  const [passwordError, setPasswordError] = useState<string | null>(null);
  const [passwordSuccess, setPasswordSuccess] = useState<string | null>(null);

  // Email change state
  const [emailForm, setEmailForm] = useState({
    password: "",
    newEmail: "",
  });
  const [emailLoading, setEmailLoading] = useState(false);
  const [emailError, setEmailError] = useState<string | null>(null);
  const [emailSuccess, setEmailSuccess] = useState<string | null>(null);

  // Account deletion state
  const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
  const [deletePassword, setDeletePassword] = useState("");
  const [deleteLoading, setDeleteLoading] = useState(false);
  const [deleteError, setDeleteError] = useState<string | null>(null);

  // Repository history state
  const [repoHistory, setRepoHistory] = useState<RepositoryHistoryEntry[]>([]);
  const [repoHistoryLoading, setRepoHistoryLoading] = useState(true);
  const [repoHistoryError, setRepoHistoryError] = useState<string | null>(null);
  const [deletingRepoId, setDeletingRepoId] = useState<string | null>(null);

  useEffect(() => {
    loadApiKey();
    loadRepoHistory();
  }, []);

  const loadApiKey = async () => {
    try {
      setLoading(true);
      setError(null);
      const key = await getApiKey();
      setApiKeyInfo(key);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to load API key");
    } finally {
      setLoading(false);
    }
  };

  const loadRepoHistory = async () => {
    try {
      setRepoHistoryError(null);
      const response = await listRepositoryHistory();
      setRepoHistory(response.entries);
    } catch (err) {
      setRepoHistoryError(err instanceof Error ? err.message : "Failed to load repository history");
    } finally {
      setRepoHistoryLoading(false);
    }
  };

  const handleDeleteRepoHistory = async (id: string) => {
    try {
      setDeletingRepoId(id);
      await deleteRepositoryHistory(id);
      setRepoHistory((prev) => prev.filter((entry) => entry.id !== id));
    } catch (err) {
      setRepoHistoryError(err instanceof Error ? err.message : "Failed to delete entry");
    } finally {
      setDeletingRepoId(null);
    }
  };

  const handleCreate = async () => {
    try {
      setActionLoading(true);
      setError(null);
      setNewKey(null);
      const response: CreateApiKeyResponse = await createApiKey("Web UI");
      setNewKey(response.key);
      setApiKeyInfo({
        id: response.id,
        prefix: response.prefix,
        name: response.name,
        lastUsedAt: null,
        createdAt: response.createdAt,
      });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to create API key");
    } finally {
      setActionLoading(false);
    }
  };

  const handleRefresh = async () => {
    try {
      setActionLoading(true);
      setError(null);
      setNewKey(null);
      const response = await refreshApiKey("Web UI (Refreshed)");
      setNewKey(response.key);
      setApiKeyInfo({
        id: response.id,
        prefix: response.prefix,
        name: response.name,
        lastUsedAt: null,
        createdAt: response.createdAt,
      });
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to refresh API key");
    } finally {
      setActionLoading(false);
    }
  };

  const handleRevoke = async () => {
    if (!confirm("Are you sure you want to revoke this API key? Any applications using it will stop working.")) {
      return;
    }
    try {
      setActionLoading(true);
      setError(null);
      setNewKey(null);
      await revokeApiKey();
      setApiKeyInfo(null);
    } catch (err) {
      setError(err instanceof Error ? err.message : "Failed to revoke API key");
    } finally {
      setActionLoading(false);
    }
  };

  const copyToClipboard = async () => {
    if (!newKey) return;
    try {
      await navigator.clipboard.writeText(newKey);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (err) {
      console.error("Failed to copy:", err);
    }
  };

  // Password change handlers
  const handlePasswordChange = async (e: FormEvent) => {
    e.preventDefault();
    setPasswordError(null);
    setPasswordSuccess(null);

    if (passwordForm.newPassword !== passwordForm.confirmPassword) {
      setPasswordError("New passwords do not match");
      return;
    }

    const strength = getPasswordStrength(passwordForm.newPassword);
    if (strength.score < 1) {
      setPasswordError("Password must be at least 6 characters");
      return;
    }

    try {
      setPasswordLoading(true);
      await changePassword(passwordForm.currentPassword, passwordForm.newPassword);
      setPasswordSuccess("Password changed successfully. Please sign in with your new password.");
      setPasswordForm({ currentPassword: "", newPassword: "", confirmPassword: "" });
      setTimeout(async () => {
        await signOut();
        navigate("/login");
      }, 1500);
    } catch (err) {
      setPasswordError(err instanceof Error ? err.message : "Failed to change password");
    } finally {
      setPasswordLoading(false);
    }
  };

  // Email change handlers
  const handleEmailChange = async (e: FormEvent) => {
    e.preventDefault();
    setEmailError(null);
    setEmailSuccess(null);

    if (!emailForm.newEmail.includes("@")) {
      setEmailError("Please enter a valid email address");
      return;
    }

    try {
      setEmailLoading(true);
      await changeEmail(emailForm.password, emailForm.newEmail);
      setEmailSuccess("Email changed successfully");
      setEmailForm({ password: "", newEmail: "" });
    } catch (err) {
      setEmailError(err instanceof Error ? err.message : "Failed to change email");
    } finally {
      setEmailLoading(false);
    }
  };

  // Account deletion handlers
  const DELETE_CONFIRMATION = "DELETE MY ACCOUNT";

  const handleDeleteAccount = async () => {
    try {
      setDeleteLoading(true);
      setDeleteError(null);
      await deleteAccount(deletePassword, DELETE_CONFIRMATION);
      await signOut();
      navigate("/login");
    } catch (err) {
      setDeleteError(err instanceof Error ? err.message : "Failed to delete account");
      setDeleteLoading(false);
    }
  };

  const passwordStrength = getPasswordStrength(passwordForm.newPassword);

  return (
    <div className="relative z-10 min-h-screen flex flex-col bg-[#0a1628]">
      <Masthead showNav />

      <main className="flex-1 max-w-4xl mx-auto p-6 w-full">
        {/* Page Header */}
        <div className="mb-8">
          <h1 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff]">Settings</h1>
          <div className="h-px bg-[rgba(117,170,252,0.35)] mt-2" />
        </div>

        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
          {/* Left Column */}
          <div className="space-y-6">
            {/* Account Info */}
            <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
              <SectionHeader>Account</SectionHeader>
              {isAuthConfigured && user ? (
                <div className="space-y-2 font-mono text-xs">
                  <div className="flex justify-between">
                    <span className="text-[#7788aa]">Email</span>
                    <span className="text-[#9bc3ff]">{user.email}</span>
                  </div>
                  <div className="flex justify-between">
                    <span className="text-[#7788aa]">User ID</span>
                    <span className="text-[#75aafc] text-[10px]">{user.id}</span>
                  </div>
                </div>
              ) : (
                <p className="text-[#7788aa] font-mono text-xs">
                  {isAuthConfigured
                    ? "Not signed in"
                    : "Authentication not configured (API key mode)"}
                </p>
              )}
            </section>

            {/* API Key Section */}
            <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
              <SectionHeader>API Key</SectionHeader>
              <p className="text-[#7788aa] font-mono text-[10px] mb-4">
                Authenticate daemon and CLI tools. One active key at a time.
              </p>

              {error && <ErrorAlert>{error}</ErrorAlert>}

              {newKey && (
                <div className="border border-green-700/50 bg-green-900/20 p-3 mb-4">
                  <p className="text-green-400 font-mono text-[10px] mb-2">
                    Key created. Copy now - won't be shown again.
                  </p>
                  <div className="flex items-center gap-2">
                    <code className="flex-1 bg-black/50 px-2 py-1 text-[10px] font-mono text-green-400 break-all">
                      {newKey}
                    </code>
                    <button
                      onClick={copyToClipboard}
                      className="px-2 py-1 bg-green-900/50 border border-green-700/50 text-green-400 font-mono text-[10px] uppercase hover:bg-green-800/50 transition-colors"
                    >
                      {copied ? "Copied" : "Copy"}
                    </button>
                  </div>
                </div>
              )}

              {loading ? (
                <p className="text-[#7788aa] font-mono text-xs">Loading...</p>
              ) : apiKeyInfo ? (
                <div className="space-y-3">
                  <div className="font-mono text-xs">
                    <div className="flex justify-between mb-1">
                      <span className="text-[#7788aa]">Prefix</span>
                      <code className="text-[#75aafc]">{apiKeyInfo.prefix}...</code>
                    </div>
                    <div className="flex justify-between mb-1">
                      <span className="text-[#7788aa]">Created</span>
                      <span className="text-[#9bc3ff]">
                        {new Date(apiKeyInfo.createdAt).toLocaleDateString()}
                      </span>
                    </div>
                    {apiKeyInfo.lastUsedAt && (
                      <div className="flex justify-between">
                        <span className="text-[#7788aa]">Last used</span>
                        <span className="text-[#9bc3ff]">
                          {new Date(apiKeyInfo.lastUsedAt).toLocaleDateString()}
                        </span>
                      </div>
                    )}
                  </div>
                  <div className="flex gap-2 pt-2">
                    <SecondaryButton onClick={handleRefresh} disabled={actionLoading}>
                      {actionLoading ? "..." : "Rotate"}
                    </SecondaryButton>
                    <DangerButton onClick={handleRevoke} disabled={actionLoading}>
                      {actionLoading ? "..." : "Revoke"}
                    </DangerButton>
                  </div>
                </div>
              ) : (
                <div>
                  <p className="text-[#7788aa] font-mono text-xs mb-3">No API key configured.</p>
                  <PrimaryButton onClick={handleCreate} disabled={actionLoading}>
                    {actionLoading ? "Creating..." : "Create API Key"}
                  </PrimaryButton>
                </div>
              )}
            </section>

            {/* Daemons Link */}
            <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
              <SectionHeader>Daemons</SectionHeader>
              <p className="text-[#7788aa] font-mono text-[10px] mb-3">
                Daemon management has moved to its own page.
              </p>
              <a href="/daemons" className="text-[#75aafc] hover:text-[#9bc3ff] text-xs font-mono">
                Go to Daemons &rarr;
              </a>
            </section>

            {/* Repository History */}
            <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
              <div className="flex items-center justify-between mb-3 pb-2 border-b border-[rgba(117,170,252,0.15)]">
                <div className="flex items-center gap-2">
                  <h2 className="text-[11px] font-mono uppercase tracking-wide text-[#8899aa]">
                    Repository History
                  </h2>
                  {repoHistory.length > 0 && (
                    <span className="text-[10px] font-mono text-[#556677]">
                      ({repoHistory.length} saved)
                    </span>
                  )}
                </div>
                <button
                  onClick={loadRepoHistory}
                  disabled={repoHistoryLoading}
                  className="text-[10px] font-mono text-[#75aafc] hover:text-[#9bc3ff] disabled:opacity-50"
                  title="Refresh"
                >
                  {repoHistoryLoading ? "..." : "↻"}
                </button>
              </div>
              <p className="text-[#7788aa] font-mono text-[10px] mb-3">
                Previously used repositories will appear as suggestions when adding repos to contracts.
              </p>

              {repoHistoryError && <ErrorAlert>{repoHistoryError}</ErrorAlert>}

              {repoHistoryLoading && repoHistory.length === 0 ? (
                <p className="text-[#7788aa] font-mono text-xs">Loading...</p>
              ) : repoHistory.length === 0 ? (
                <div className="text-center py-4">
                  <p className="text-[#7788aa] font-mono text-xs mb-2">No repository history</p>
                  <p className="text-[#556677] font-mono text-[10px]">
                    Add repositories to contracts to build history
                  </p>
                </div>
              ) : (
                <div className="space-y-2 max-h-[300px] overflow-y-auto">
                  {repoHistory.map((entry) => (
                    <div
                      key={entry.id}
                      className="border border-[rgba(117,170,252,0.15)] bg-[#0a1525] p-3"
                    >
                      <div className="flex items-start justify-between gap-2">
                        <div className="flex-1 min-w-0">
                          <div className="flex items-center gap-2 mb-1">
                            <span className="font-mono text-xs text-[#9bc3ff] truncate">
                              {entry.name}
                            </span>
                            <span
                              className={`text-[10px] font-mono uppercase px-1.5 py-0.5 border ${
                                entry.sourceType === "remote"
                                  ? "text-cyan-400 border-cyan-700/50 bg-cyan-900/20"
                                  : "text-green-400 border-green-700/50 bg-green-900/20"
                              }`}
                            >
                              {entry.sourceType}
                            </span>
                          </div>
                          <div className="font-mono text-[10px] text-[#556677] truncate mb-1">
                            {entry.repositoryUrl || entry.localPath}
                          </div>
                          <div className="flex items-center gap-3 font-mono text-[10px] text-[#7788aa]">
                            <span>Used {entry.useCount}×</span>
                            <span>
                              Last: {new Date(entry.lastUsedAt).toLocaleDateString()}
                            </span>
                          </div>
                        </div>
                        <button
                          onClick={() => handleDeleteRepoHistory(entry.id)}
                          disabled={deletingRepoId === entry.id}
                          className="p-1 font-mono text-[10px] text-[#555] hover:text-red-400 transition-colors disabled:opacity-50"
                          title="Delete from history"
                        >
                          {deletingRepoId === entry.id ? "..." : "×"}
                        </button>
                      </div>
                    </div>
                  ))}
                </div>
              )}
            </section>
          </div>

          {/* Right Column */}
          <div className="space-y-6">
            {/* Password Change */}
            {isAuthConfigured && user && (
              <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
                <SectionHeader>Change Password</SectionHeader>
                {passwordError && <ErrorAlert>{passwordError}</ErrorAlert>}
                {passwordSuccess && <SuccessAlert>{passwordSuccess}</SuccessAlert>}
                <form onSubmit={handlePasswordChange} className="space-y-3">
                  <FormInput
                    label="Current Password"
                    type="password"
                    value={passwordForm.currentPassword}
                    onChange={(v) => setPasswordForm({ ...passwordForm, currentPassword: v })}
                    required
                  />
                  <FormInput
                    label="New Password"
                    type="password"
                    value={passwordForm.newPassword}
                    onChange={(v) => setPasswordForm({ ...passwordForm, newPassword: v })}
                    required
                  />
                  {passwordForm.newPassword && (
                    <div className="space-y-1">
                      <div className="flex items-center gap-2">
                        <div className="flex-1 h-1 bg-[#1a2a3a]">
                          <div
                            className={`h-full transition-all ${passwordStrength.color}`}
                            style={{ width: `${passwordStrength.score * 100}%` }}
                          />
                        </div>
                        <span className="text-[10px] font-mono text-[#9bc3ff]">
                          {passwordStrength.label}
                        </span>
                      </div>
                    </div>
                  )}
                  <FormInput
                    label="Confirm Password"
                    type="password"
                    value={passwordForm.confirmPassword}
                    onChange={(v) => setPasswordForm({ ...passwordForm, confirmPassword: v })}
                    required
                  />
                  {passwordForm.confirmPassword &&
                    passwordForm.newPassword !== passwordForm.confirmPassword && (
                      <p className="text-red-400 font-mono text-[10px]">Passwords do not match</p>
                    )}
                  <div className="pt-2">
                    <PrimaryButton
                      type="submit"
                      disabled={passwordLoading || passwordStrength.score < 1}
                    >
                      {passwordLoading ? "Changing..." : "Change Password"}
                    </PrimaryButton>
                  </div>
                </form>
              </section>
            )}

            {/* Email Change */}
            {isAuthConfigured && user && (
              <section className="border border-[rgba(117,170,252,0.25)] bg-[#0d1b2d] p-4">
                <SectionHeader>Change Email</SectionHeader>
                {emailError && <ErrorAlert>{emailError}</ErrorAlert>}
                {emailSuccess && <SuccessAlert>{emailSuccess}</SuccessAlert>}
                <form onSubmit={handleEmailChange} className="space-y-3">
                  <FormInput
                    label="New Email"
                    type="email"
                    value={emailForm.newEmail}
                    onChange={(v) => setEmailForm({ ...emailForm, newEmail: v })}
                    placeholder="new@example.com"
                    required
                  />
                  <FormInput
                    label="Password (to confirm)"
                    type="password"
                    value={emailForm.password}
                    onChange={(v) => setEmailForm({ ...emailForm, password: v })}
                    required
                  />
                  <div className="pt-2">
                    <PrimaryButton type="submit" disabled={emailLoading}>
                      {emailLoading ? "Changing..." : "Change Email"}
                    </PrimaryButton>
                  </div>
                </form>
              </section>
            )}

            {/* Danger Zone */}
            {isAuthConfigured && user && (
              <section className="border border-red-900/50 bg-[#0d1b2d] p-4">
                <h2 className="text-[11px] font-mono uppercase tracking-wide text-red-400 mb-3 pb-2 border-b border-red-900/30">
                  Danger Zone
                </h2>
                <p className="text-[#7788aa] font-mono text-[10px] mb-3">
                  Permanently delete your account and all data. This cannot be undone.
                </p>
                {deleteError && <ErrorAlert>{deleteError}</ErrorAlert>}
                <div className="space-y-3">
                  <FormInput
                    label="Password"
                    type="password"
                    value={deletePassword}
                    onChange={setDeletePassword}
                    placeholder="Enter password to continue"
                  />
                  <DangerButton
                    onClick={() => setDeleteDialogOpen(true)}
                    disabled={!deletePassword || deleteLoading}
                  >
                    {deleteLoading ? "Deleting..." : "Delete Account"}
                  </DangerButton>
                </div>
              </section>
            )}
          </div>
        </div>
      </main>

      {/* Delete Confirmation Dialog */}
      <ConfirmDialog
        isOpen={deleteDialogOpen}
        title="Delete Account"
        message="This will permanently delete your account and all your data. This action cannot be undone."
        confirmText="Delete"
        confirmButtonClass="bg-red-900/50 border border-red-700 text-red-400 hover:bg-red-800/50"
        requireInput={DELETE_CONFIRMATION}
        inputPlaceholder={`Type "${DELETE_CONFIRMATION}" to confirm`}
        onConfirm={() => {
          setDeleteDialogOpen(false);
          handleDeleteAccount();
        }}
        onCancel={() => setDeleteDialogOpen(false)}
      />
    </div>
  );
}