Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a React 19/Vite SPA. Mail (conversation view, search operators, labels, sanitised HTML, privacy image proxy, invites, undo send, templates), calendar (month/week/day/agenda, invites, free/busy, categories, context menus), contacts (JSContact, groups, vCard), files, Sieve filter builder (incl. filter-from-message with retroactive apply), vacation, identities with default + Reply-To, PWA/mobile layout, push via SSE, in-memory mock Stalwart for dev, Docker + CI.
63 lines
3.8 KiB
TypeScript
63 lines
3.8 KiB
TypeScript
import { lazy, Suspense, type ReactNode } from "react";
|
|
import { Link, useLocation } from "wouter";
|
|
import { ArrowLeft, Bell, Filter, Folder, Info, Keyboard, LayoutTemplate, Palette, PenLine, Plane, Settings as SettingsIcon, ShieldCheck, Tag, Users, Calendar } from "lucide-react";
|
|
import { Spinner } from "@/ui/misc";
|
|
import { GeneralSettings } from "./GeneralSettings";
|
|
import { AppearanceSettings } from "./AppearanceSettings";
|
|
import { IdentitiesSettings } from "./IdentitiesSettings";
|
|
import { FoldersSettings } from "./FoldersSettings";
|
|
import { LabelsSettings } from "./LabelsSettings";
|
|
import { TemplatesSettings } from "./TemplatesSettings";
|
|
import { NotificationsSettings } from "./NotificationsSettings";
|
|
import { SecuritySettings } from "./SecuritySettings";
|
|
import { AboutSettings } from "./AboutSettings";
|
|
import { ShortcutsSettings } from "./ShortcutsSettings";
|
|
import { CalendarSettings } from "./CalendarSettings";
|
|
|
|
const FiltersSettings = lazy(() => import("./FiltersSettings").then((m) => ({ default: m.FiltersSettings })));
|
|
const VacationSettings = lazy(() => import("./VacationSettings").then((m) => ({ default: m.VacationSettings })));
|
|
|
|
const SECTIONS: Array<{ id: string; label: string; icon: ReactNode; el: ReactNode }> = [
|
|
{ id: "general", label: "General", icon: <SettingsIcon size={18} />, el: <GeneralSettings /> },
|
|
{ id: "appearance", label: "Appearance", icon: <Palette size={18} />, el: <AppearanceSettings /> },
|
|
{ id: "identities", label: "Identities & signatures", icon: <PenLine size={18} />, el: <IdentitiesSettings /> },
|
|
{ id: "filters", label: "Filters & rules", icon: <Filter size={18} />, el: <FiltersSettings /> },
|
|
{ id: "vacation", label: "Out of office", icon: <Plane size={18} />, el: <VacationSettings /> },
|
|
{ id: "folders", label: "Folders", icon: <Folder size={18} />, el: <FoldersSettings /> },
|
|
{ id: "labels", label: "Labels", icon: <Tag size={18} />, el: <LabelsSettings /> },
|
|
{ id: "templates", label: "Templates", icon: <LayoutTemplate size={18} />, el: <TemplatesSettings /> },
|
|
{ id: "calendar", label: "Calendar & contacts", icon: <Calendar size={18} />, el: <CalendarSettings /> },
|
|
{ id: "notifications", label: "Notifications", icon: <Bell size={18} />, el: <NotificationsSettings /> },
|
|
{ id: "security", label: "Security & sessions", icon: <ShieldCheck size={18} />, el: <SecuritySettings /> },
|
|
{ id: "shortcuts", label: "Keyboard shortcuts", icon: <Keyboard size={18} />, el: <ShortcutsSettings /> },
|
|
{ id: "about", label: "About", icon: <Info size={18} />, el: <AboutSettings /> },
|
|
];
|
|
|
|
export function SettingsView({ section }: { section?: string }) {
|
|
const [, navigate] = useLocation();
|
|
const current = SECTIONS.find((s) => s.id === section);
|
|
return (
|
|
<div className={`settings-layout ${section ? "section" : "root"}`}>
|
|
<nav className="settings-nav" aria-label="Settings">
|
|
<div className="nav-section" style={{ paddingLeft: 8 }}><span>Settings</span></div>
|
|
{SECTIONS.map((s) => (
|
|
<Link key={s.id} href={`/settings/${s.id}`} className={`nav-item ${section === s.id ? "active" : ""}`}>
|
|
{s.icon}
|
|
<span className="nav-label">{s.label}</span>
|
|
</Link>
|
|
))}
|
|
<div className="nav-section" style={{ paddingLeft: 8 }}><span>Shortcuts</span></div>
|
|
<Link href="/contacts" className="nav-item"><Users size={18} /><span className="nav-label">Address books</span></Link>
|
|
</nav>
|
|
<div className="settings-content">
|
|
{section && (
|
|
<button className="btn btn-ghost btn-sm" style={{ marginBottom: 8, marginLeft: -8 }} onClick={() => navigate("/settings")}>
|
|
<ArrowLeft size={16} /> All settings
|
|
</button>
|
|
)}
|
|
<Suspense fallback={<Spinner />}>{current ? current.el : <GeneralSettings />}</Suspense>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|