Files
EpicNext-Cms/src/features/housekeeping/foundation/shell/widget-settings.tsx
T

138 lines
3.8 KiB
TypeScript

"use client";
import { useState } from "react";
import type {
HousekeepingResult,
HousekeepingWidgetDefinition,
} from "../contracts";
import type { HousekeepingPreferences } from "../preferences/schema";
export type HousekeepingPreferenceSaver = (
preferences: HousekeepingPreferences,
) => Promise<HousekeepingResult<HousekeepingPreferences>>;
export function toggleOptionalWidget(
preferences: HousekeepingPreferences,
widgetId: string,
): HousekeepingPreferences {
const enabled = preferences.enabledOptionalWidgetIds.includes(widgetId);
return {
...preferences,
widgetOrder:
!enabled && !preferences.widgetOrder.includes(widgetId)
? [...preferences.widgetOrder, widgetId]
: [...preferences.widgetOrder],
enabledOptionalWidgetIds: enabled
? preferences.enabledOptionalWidgetIds.filter((id) => id !== widgetId)
: [...preferences.enabledOptionalWidgetIds, widgetId],
};
}
export function preferencesAfterSave(
current: HousekeepingPreferences,
_proposed: HousekeepingPreferences,
result: HousekeepingResult<HousekeepingPreferences>,
): { preferences: HousekeepingPreferences; error?: string } {
return result.ok
? { preferences: result.data }
: { preferences: current, error: result.error.messageKey };
}
export interface WidgetSettingsLabels {
readonly title: string;
readonly mandatory: string;
readonly error: string;
}
export function WidgetSettings({
preferences: initialPreferences,
widgets,
savePreferences,
labels,
titles = {},
}: {
readonly preferences: HousekeepingPreferences;
readonly widgets: readonly HousekeepingWidgetDefinition[];
readonly savePreferences: HousekeepingPreferenceSaver;
readonly labels: WidgetSettingsLabels;
readonly titles?: Readonly<Record<string, string>>;
}) {
const [preferences, setPreferences] = useState(initialPreferences);
const [savingId, setSavingId] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const toggle = async (widgetId: string) => {
const proposed = toggleOptionalWidget(preferences, widgetId);
setSavingId(widgetId);
setError(null);
try {
const outcome = preferencesAfterSave(
preferences,
proposed,
await savePreferences(proposed),
);
setPreferences(outcome.preferences);
setError(outcome.error ?? null);
} catch {
setError(labels.error);
} finally {
setSavingId(null);
}
};
return (
<section
aria-labelledby="housekeeping-widget-settings-title"
className="space-y-3"
>
<h2
id="housekeeping-widget-settings-title"
className="text-lg font-semibold"
>
{labels.title}
</h2>
{error ? (
<p
role="alert"
aria-live="assertive"
className="text-[var(--admin-danger)]"
>
{error === labels.error ? error : labels.error}
</p>
) : null}
<ul className="space-y-2">
{widgets.map((widget) => {
const mandatory = widget.kind === "mandatory";
const checked =
mandatory ||
preferences.enabledOptionalWidgetIds.includes(widget.id);
return (
<li
key={widget.id}
className="flex min-w-0 items-center justify-between gap-3 rounded-md border border-[var(--admin-border)] p-3"
>
<label className="flex min-w-0 items-center gap-2">
<input
type="checkbox"
checked={checked}
disabled={mandatory || savingId !== null}
onChange={() => void toggle(widget.id)}
className="focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--admin-accent)]"
/>
<span className="min-w-0 break-words">
{titles[widget.id] ?? widget.id}
</span>
</label>
{mandatory ? (
<span className="text-xs text-[var(--admin-text-muted)]">
{labels.mandatory}
</span>
) : null}
</li>
);
})}
</ul>
</section>
);
}