ihasmail 2.0: rebuild as Stalwart-first JMAP webmail
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.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user