feat(housekeeping): personalize command deck
This commit is contained in:
1 parent
300ac0ef95
commit
5574e601bb
12 files changed
+1549
-1
No files matched your search
@@ -0,0 +1,130 @@
|
||||
"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" 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 items-center justify-between gap-3 rounded-md border border-[var(--admin-border)] p-3"
|
||||
>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
disabled={mandatory || savingId !== null}
|
||||
onChange={() => void toggle(widget.id)}
|
||||
/>
|
||||
<span>{titles[widget.id] ?? widget.id}</span>
|
||||
</label>
|
||||
{mandatory ? (
|
||||
<span className="text-xs text-[var(--admin-text-muted)]">
|
||||
{labels.mandatory}
|
||||
</span>
|
||||
) : null}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user