mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
feat(modules): unify Vorsorge, Illness and Labs with the medication pattern
- add a wrench beside each module's add button linking to the module's own settings page; rename every add button to a plain "hinzufügen" - give each module a settings page: view (cards/list) + manual reorder; Labs also gets a sort-order choice (by last reading) and biomarker CRUD/reorder - collapse the Illness "abgeschlossen" list by default - rebuild the Vorsorge card on the medication card's building blocks: a green "Jetzt messen" action, the interval as a chip beside the name, and an in-interval progress signal; drop the kebab "anpassen" and stray actions - persist per-module view/order/sort client-side; add reverse-coverage-clean strings across all six locales
This commit is contained in:
1 parent
ae3091df9e
commit
267dcad13d
22 files changed
+1834
-273
No files matched your search
@@ -0,0 +1,221 @@
|
||||
"use client";
|
||||
|
||||
import { useId } from "react";
|
||||
import { ArrowDown, ArrowUp, GripVertical } from "lucide-react";
|
||||
import {
|
||||
DndContext,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
closestCenter,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { reorderById } from "@/lib/insights-layout-reorder";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* v1.18.6 (W8 / MOD-03) — generic, controlled reorder list shared by the
|
||||
* Vorsorge / Illness / Labs settings pages.
|
||||
*
|
||||
* A single section (no active/inactive split — the secondary modules have a
|
||||
* flat list) with the same vocabulary as the medication order editor: an
|
||||
* `@dnd-kit` drag handle (pointer + keyboard sensors) plus per-row up/down
|
||||
* arrow buttons as the primary keyboard surface. Unlike the medication
|
||||
* editor it is fully controlled — there is no Save/Cancel draft state
|
||||
* because the host persists each move immediately to the localStorage order
|
||||
* (the choice is presentational, not a server write). The swap-and-renumber
|
||||
* logic is the shared `reorderById` helper so the order semantics match the
|
||||
* dashboard / insights / medications surfaces exactly.
|
||||
*/
|
||||
|
||||
export interface ReorderItem {
|
||||
id: string;
|
||||
/** Primary label (bold). */
|
||||
name: string;
|
||||
/** Optional secondary line (muted). */
|
||||
secondary?: string;
|
||||
}
|
||||
|
||||
interface ModuleOrderEditorProps {
|
||||
/** The list in its current effective order. */
|
||||
items: ReorderItem[];
|
||||
/** Persist the full id order after every move. */
|
||||
onChange: (order: string[]) => void;
|
||||
}
|
||||
|
||||
export function ModuleOrderEditor({ items, onChange }: ModuleOrderEditorProps) {
|
||||
const { t } = useTranslations();
|
||||
const dragHintId = useId();
|
||||
const ids = items.map((i) => i.id);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
|
||||
const labels = {
|
||||
moveUp: t("dashboard.moveUp"),
|
||||
moveDown: t("dashboard.moveDown"),
|
||||
dragHandle: t("dashboard.dragHandle"),
|
||||
};
|
||||
|
||||
function applyReorder(fromId: string, toId: string) {
|
||||
onChange(
|
||||
reorderById(
|
||||
ids.map((id, order) => ({ id, order })),
|
||||
fromId,
|
||||
toId,
|
||||
).map((r) => r.id),
|
||||
);
|
||||
}
|
||||
|
||||
function move(id: string, delta: -1 | 1) {
|
||||
const idx = ids.indexOf(id);
|
||||
const targetIdx = idx + delta;
|
||||
if (idx < 0 || targetIdx < 0 || targetIdx >= ids.length) return;
|
||||
applyReorder(id, ids[targetIdx]);
|
||||
}
|
||||
|
||||
function handleDragEnd(event: DragEndEvent) {
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
applyReorder(String(active.id), String(over.id));
|
||||
}
|
||||
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("moduleList.reorder.empty")}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2" data-slot="module-order-editor">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
|
||||
{items.map((item, index) => (
|
||||
<SortableRow
|
||||
key={item.id}
|
||||
item={item}
|
||||
index={index}
|
||||
total={items.length}
|
||||
dragHintId={dragHintId}
|
||||
labels={labels}
|
||||
onMove={move}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
<p id={dragHintId} className="text-muted-foreground sr-only">
|
||||
{t("dashboard.dragHandleHint")}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface SortableRowProps {
|
||||
item: ReorderItem;
|
||||
index: number;
|
||||
total: number;
|
||||
dragHintId: string;
|
||||
labels: { moveUp: string; moveDown: string; dragHandle: string };
|
||||
onMove: (id: string, delta: -1 | 1) => void;
|
||||
}
|
||||
|
||||
function SortableRow({
|
||||
item,
|
||||
index,
|
||||
total,
|
||||
dragHintId,
|
||||
labels,
|
||||
onMove,
|
||||
}: SortableRowProps) {
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
isDragging,
|
||||
} = useSortable({ id: item.id });
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition: prefersReducedMotion() ? "none" : transition,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
data-slot="module-reorder-row"
|
||||
data-dragging={isDragging ? "true" : undefined}
|
||||
className={`border-border bg-background/30 flex min-h-12 items-center gap-2 rounded-md border px-3 py-2 ${
|
||||
isDragging ? "ring-primary z-10 opacity-90 shadow-lg ring-2" : ""
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
aria-label={`${labels.dragHandle} — ${item.name}`}
|
||||
aria-describedby={dragHintId}
|
||||
title={labels.dragHandle}
|
||||
data-slot="module-drag-handle"
|
||||
className="text-muted-foreground hover:text-foreground focus-visible:ring-ring focus-visible:ring-offset-background relative -m-1 inline-flex h-7 w-7 cursor-grab touch-none items-center justify-center rounded transition-colors before:absolute before:inset-[-8px] before:content-[''] focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none active:cursor-grabbing motion-reduce:transition-none"
|
||||
>
|
||||
<GripVertical className="h-4 w-4" />
|
||||
</button>
|
||||
<span className="min-w-0 flex-1 text-sm">
|
||||
<span className="block truncate font-medium" title={item.name}>
|
||||
{item.name}
|
||||
</span>
|
||||
{item.secondary ? (
|
||||
<span className="text-muted-foreground block truncate text-xs">
|
||||
{item.secondary}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-11 sm:size-9"
|
||||
onClick={() => onMove(item.id, -1)}
|
||||
disabled={index === 0}
|
||||
aria-label={`${labels.moveUp} — ${item.name}`}
|
||||
>
|
||||
<ArrowUp className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-11 sm:size-9"
|
||||
onClick={() => onMove(item.id, 1)}
|
||||
disabled={index === total - 1}
|
||||
aria-label={`${labels.moveDown} — ${item.name}`}
|
||||
>
|
||||
<ArrowDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user