138 lines
3.8 KiB
TypeScript
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>
|
|
);
|
|
}
|