mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
The old panel kept its active tab in a store, so every page sat at /admin, the sidebar duplicated the webmail rail by hand, and each page was a stack of boxed cards with their own save buttons. Plugin settings were only reachable through a gear icon in a row of five. - one catch-all route for the whole panel; pages switch through history.pushState, so `/admin/plugins/<id>`, the back button, bookmarks and connector links all work without a route transition - sidebar laid out like the user settings: back to mail, search over pages and their sections, two group headings, change password and sign out at the bottom; the phone shows a list, then the page - pages use SettingsSection rows instead of nested cards, with one page header, one flash message and one save bar at the bottom (Discard, Save, Ctrl+S); the unsaved-changes prompt still guards leaving - plugins and themes are lists of rows with a toggle each; a row opens the extension's own page with its settings, force-on, permissions, kept versions and removal - overview shows stat tiles, server facts and recent activity - old `/admin?tab=x` links redirect; the tab store is gone and the connector registry resolves admin links to the page URL - `NEXT_TURBOPACK_ROOT` overrides the Turbopack root so `next dev` runs from a worktree with a junctioned node_modules
58 lines
1.9 KiB
TypeScript
58 lines
1.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, type Dispatch, type SetStateAction } from 'react';
|
|
import { CheckCircle2, AlertTriangle, X } from '@/components/icons';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export type FlashMessage = { type: 'success' | 'error'; text: string } | null;
|
|
|
|
/** Page-level feedback state; the message is shown with `<Flash>`. */
|
|
export function useFlash(): [FlashMessage, Dispatch<SetStateAction<FlashMessage>>] {
|
|
return useState<FlashMessage>(null);
|
|
}
|
|
|
|
interface FlashProps {
|
|
message: FlashMessage;
|
|
onDismiss: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* The outcome of the last action, under the page header. A success goes
|
|
* away by itself; an error stays until dismissed or the next action.
|
|
*/
|
|
export function Flash({ message, onDismiss, className }: FlashProps) {
|
|
useEffect(() => {
|
|
if (message?.type !== 'success') return;
|
|
const timer = setTimeout(onDismiss, 6000);
|
|
return () => clearTimeout(timer);
|
|
}, [message, onDismiss]);
|
|
|
|
if (!message) return null;
|
|
const success = message.type === 'success';
|
|
const Icon = success ? CheckCircle2 : AlertTriangle;
|
|
return (
|
|
<div
|
|
role={success ? 'status' : 'alert'}
|
|
className={cn(
|
|
'mb-6 flex items-start gap-2.5 rounded-md border px-3 py-2.5 text-sm',
|
|
success
|
|
? 'border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950/30 dark:text-emerald-300'
|
|
: 'border-destructive/30 bg-destructive/10 text-destructive',
|
|
className,
|
|
)}
|
|
>
|
|
<Icon className="w-4 h-4 shrink-0 mt-0.5" />
|
|
<span className="flex-1 min-w-0 break-words">{message.text}</span>
|
|
<button
|
|
type="button"
|
|
onClick={onDismiss}
|
|
aria-label="Dismiss"
|
|
className="shrink-0 -me-1 rounded p-0.5 opacity-70 hover:opacity-100 transition-opacity"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|