6f35eb77ae
- Add HomePage with open sessions grid, projects overview, and session composer - Add SettingsPage with tabs for General, SSH Keys, Tool Types, Tool Configs - Update navigation to Home, Projects, Settings - Redirect legacy routes (/sessions, /ssh-keys, /tool-types, /tool-configs) - Apply Inter font and warm editorial styling - Update tests for new dashboard and projects pages Quality gates: typecheck pass, lint pass, 15/15 tests pass Refs: openspec/changes/ui-redesign-home-settings
167 lines
5.6 KiB
TypeScript
167 lines
5.6 KiB
TypeScript
import { useCallback, useEffect, useState } from "react";
|
|
import { Link, Outlet, useLocation, useOutletContext } from "react-router-dom";
|
|
|
|
import { getUserConfig, updateUserConfig, type UserConfig, type UserConfigUpdate } from "../api/settings";
|
|
import { Icon } from "../components/icon";
|
|
|
|
type SettingsStatus = "loading" | "ready" | "error";
|
|
|
|
const TABS = [
|
|
{ label: "General", path: "general" },
|
|
{ label: "SSH Keys", path: "ssh-keys" },
|
|
{ label: "Tool Types", path: "tool-types" },
|
|
{ label: "Tool Configs", path: "tool-configs" },
|
|
] as const;
|
|
|
|
const THEME_OPTIONS = [
|
|
{ value: "system", label: "System" },
|
|
{ value: "light", label: "Light" },
|
|
{ value: "dark", label: "Dark" },
|
|
];
|
|
|
|
type SettingsOutletContext = {
|
|
config: UserConfig;
|
|
handleChange: (key: keyof UserConfigUpdate, value: string | null) => void;
|
|
handleSave: () => Promise<void>;
|
|
saveStatus: "idle" | "saving" | "saved" | "error";
|
|
};
|
|
|
|
export const SettingsPage = () => {
|
|
const location = useLocation();
|
|
const [status, setStatus] = useState<SettingsStatus>("loading");
|
|
const [config, setConfig] = useState<UserConfig>({
|
|
theme: "system",
|
|
default_editor: null,
|
|
git_user_name: null,
|
|
git_user_email: null,
|
|
last_session_id: null,
|
|
});
|
|
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
|
|
|
|
const loadConfig = useCallback(async () => {
|
|
try {
|
|
const data = await getUserConfig();
|
|
setConfig(data);
|
|
setStatus("ready");
|
|
} catch {
|
|
setStatus("error");
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void loadConfig();
|
|
}, [loadConfig]);
|
|
|
|
const handleChange = (key: keyof UserConfigUpdate, value: string | null) => {
|
|
setConfig((prev) => ({ ...prev, [key]: value }));
|
|
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,
|
|
};
|
|
const updated = await updateUserConfig(update);
|
|
setConfig(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"><p className="muted">Loading settings...</p></section>;
|
|
}
|
|
|
|
if (status === "error") {
|
|
return (
|
|
<section className="stack">
|
|
<p>Failed to load settings</p>
|
|
<button className="secondary-button" onClick={() => void loadConfig()} type="button">
|
|
<Icon name="refresh" size="sm" />
|
|
Retry
|
|
</button>
|
|
</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, tool types, and tool configs live here.</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>
|
|
);
|
|
};
|
|
|
|
export const GeneralSettingsTab = () => {
|
|
const { config, handleChange, handleSave, saveStatus } = useOutletContext<SettingsOutletContext>();
|
|
|
|
return (
|
|
<div className="stack">
|
|
<h2>General</h2>
|
|
<label className="form-field">
|
|
Theme
|
|
<select value={config.theme} onChange={(e) => handleChange("theme", e.target.value)}>
|
|
{THEME_OPTIONS.map((opt) => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
<label className="form-field">
|
|
Git user name
|
|
<input type="text" value={config.git_user_name ?? ""} onChange={(e) => handleChange("git_user_name", e.target.value || null)} placeholder="Your git commit name" />
|
|
</label>
|
|
<label className="form-field">
|
|
Git user email
|
|
<input type="email" value={config.git_user_email ?? ""} onChange={(e) => handleChange("git_user_email", e.target.value || null)} placeholder="your.email@example.com" />
|
|
</label>
|
|
<label className="form-field">
|
|
Default editor
|
|
<input type="text" value={config.default_editor ?? ""} onChange={(e) => handleChange("default_editor", e.target.value || null)} placeholder="e.g., vscode, vim, cursor" />
|
|
</label>
|
|
<div className="settings-actions">
|
|
<button className="primary-button" onClick={() => void handleSave()} type="button">
|
|
{saveStatus === "saving" ? <><Icon name="loading" size="sm" /> Saving...</> : <><Icon name="save" size="sm" /> Save Settings</>}
|
|
</button>
|
|
{saveStatus === "saved" && <span className="success-text">Settings saved!</span>}
|
|
{saveStatus === "error" && <span className="error-text">Failed to save</span>}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|