Files
headquarter/apps/web/src/pages/SettingsPage.tsx
T
Developer e49d049455 refactor: extract SettingsPage GeneralSettingsTab
- Move GeneralSettingsTab to components/features/settings/
- Re-export from page for backward compatibility
- Slim SettingsPage from 284 to ~150 lines

Quality gates: tsc --noEmit passes, npm run build passes
2026-06-05 19:20:36 +00:00

138 lines
3.6 KiB
TypeScript

import { useEffect, useState } from "react";
import { Link, Outlet, useLocation } from "react-router-dom";
import {
getUserConfig,
updateUserConfig,
type UserConfig,
type UserConfigUpdate,
} from "../api/settings";
import { ErrorState, LoadingState } from "../components/data-states";
import { useAsyncData } from "../hooks/use-async-data";
const TABS = [
{ label: "General", path: "general" },
{ label: "SSH Keys", path: "ssh-keys" },
] as const;
export { GeneralSettingsTab } from "../components/features/settings/GeneralSettingsTab";
export const SettingsPage = () => {
const location = useLocation();
const {
data: loadedConfig,
status,
reload,
} = useAsyncData<UserConfig>(getUserConfig, []);
const [config, setConfig] = useState<UserConfig>({
theme: "system",
default_editor: null,
git_user_name: null,
git_user_email: null,
last_session_id: null,
notification_toast_level: "all",
notification_mute_categories: [],
});
const [saveStatus, setSaveStatus] = useState<
"idle" | "saving" | "saved" | "error"
>("idle");
useEffect(() => {
if (loadedConfig) {
setConfig({
...loadedConfig,
notification_toast_level:
loadedConfig.notification_toast_level ?? "all",
notification_mute_categories:
loadedConfig.notification_mute_categories ?? [],
});
}
}, [loadedConfig]);
const handleChange = (
key: keyof UserConfigUpdate,
value: string | string[] | null,
) => {
setConfig((prev) => ({ ...prev, [key]: value }) as UserConfig);
setSaveStatus("idle");
};
const handleSave = async () => {
setSaveStatus("saving");
try {
const update: UserConfigUpdate = {
theme: config.theme,
default_editor: config.default_editor,
git_user_name: config.git_user_name,
git_user_email: config.git_user_email,
notification_toast_level: config.notification_toast_level,
notification_mute_categories: config.notification_mute_categories,
};
const updated = await updateUserConfig(update);
setConfig(updated);
window.dispatchEvent(
new CustomEvent("userconfig:updated", { detail: updated }),
);
setSaveStatus("saved");
if (updated.theme === "system") {
document.documentElement.removeAttribute("data-theme");
} else {
document.documentElement.setAttribute("data-theme", updated.theme);
}
window.setTimeout(() => setSaveStatus("idle"), 2000);
} catch {
setSaveStatus("error");
}
};
if (status === "loading") {
return (
<section className="stack">
<LoadingState message="Loading settings..." />
</section>
);
}
if (status === "error") {
return (
<section className="stack">
<ErrorState message="Failed to load settings" onRetry={reload} />
</section>
);
}
const parts = location.pathname.split("/").filter(Boolean);
const activePath = location.pathname.endsWith("/settings")
? "general"
: (parts[parts.length - 1] ?? "general");
return (
<section className="stack settings-page">
<header className="settings-header card stack-sm">
<div>
<p className="eyebrow">Configuration</p>
<h1>Settings</h1>
</div>
<p className="muted">
General preferences, SSH keys, and config profiles.
</p>
</header>
<nav className="settings-tabs" aria-label="Settings sections">
{TABS.map((tab) => (
<Link
key={tab.path}
className={`settings-tab ${activePath === tab.path ? "active" : ""}`}
to={tab.path === "general" ? "/settings" : `/settings/${tab.path}`}
>
{tab.label}
</Link>
))}
</nav>
<div className="settings-panel card">
<Outlet context={{ config, handleChange, handleSave, saveStatus }} />
</div>
</section>
);
};