From 9c748382b974ff4fbd0272b8022f9659f7269b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 28 Jun 2026 13:11:03 +0200 Subject: [PATCH] feat(tables): share column-sort state across the value lists Lift the byte-identical sortBy/sortDir/toggleSort triple the measurement and mood lists each carried inline into a shared useTableSort hook. Both lists now compose their page-reset and selection-clear side effects around the hook's toggle rather than re-implementing the direction math, and the date columns keep their descending-first default via descColumns. --- .../measurements/measurement-list.tsx | 27 +++++++--- src/components/mood/mood-list.tsx | 26 +++++++--- src/hooks/use-table-sort.ts | 51 +++++++++++++++++++ 3 files changed, 88 insertions(+), 16 deletions(-) create mode 100644 src/hooks/use-table-sort.ts diff --git a/src/components/measurements/measurement-list.tsx b/src/components/measurements/measurement-list.tsx index 58a194307..8d7f918eb 100644 --- a/src/components/measurements/measurement-list.tsx +++ b/src/components/measurements/measurement-list.tsx @@ -52,6 +52,7 @@ import { MoreHorizontal, } from "lucide-react"; import { Fragment, useCallback, useId, useState } from "react"; +import { useTableSort } from "@/hooks/use-table-sort"; import { createPortal } from "react-dom"; import { toast } from "sonner"; import { formatDateOrRelative, formatDateTime } from "@/lib/format"; @@ -105,6 +106,10 @@ import { */ const CUMULATIVE_TYPES = new Set(CUMULATIVE_DAY_SUM_TYPES); +// Columns that open descending when first selected. The date column reads +// newest-first by default; every other column opens ascending. +const MEASUREMENT_DESC_COLUMNS: ReadonlySet = new Set(["measuredAt"]); + interface Measurement { id: string; type: string; @@ -313,8 +318,16 @@ export function MeasurementList({ const [valueMinInput, setValueMinInputRaw] = useState(""); const [valueMaxInput, setValueMaxInputRaw] = useState(""); const [page, setPage] = useState(1); - const [sortBy, setSortBy] = useState("measuredAt"); - const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); + // Shared column-sort state (measuredAt opens descending). + const { + sortBy, + sortDir, + toggleSort: applySort, + } = useTableSort({ + defaultColumn: "measuredAt", + defaultDir: "desc", + descColumns: MEASUREMENT_DESC_COLUMNS, + }); // v1.15.13 — page-scoped multi-select. Holds the ids selected on the // CURRENT page; cleared on any page / filter / sort change (per the @@ -399,13 +412,11 @@ export function MeasurementList({ clearSelection(); }; + // Compose the page-reset + selection-clear side effects around the + // shared sort toggle (the rows the selection referred to are about to + // re-order and the list snaps back to page 1). function toggleSort(column: string) { - if (sortBy === column) { - setSortDir((d) => (d === "asc" ? "desc" : "asc")); - } else { - setSortBy(column); - setSortDir(column === "measuredAt" ? "desc" : "asc"); - } + applySort(column); setPage(1); clearSelection(); } diff --git a/src/components/mood/mood-list.tsx b/src/components/mood/mood-list.tsx index 772f902f6..00868224b 100644 --- a/src/components/mood/mood-list.tsx +++ b/src/components/mood/mood-list.tsx @@ -51,6 +51,11 @@ import { } from "lucide-react"; import { useCallback, useEffect, useId, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import { useTableSort } from "@/hooks/use-table-sort"; + +// Columns that open descending when first selected. The logged-at column +// reads newest-first by default; every other column opens ascending. +const MOOD_DESC_COLUMNS: ReadonlySet = new Set(["moodLoggedAt"]); import { toast } from "sonner"; import { formatDateTime } from "@/lib/format"; import { useTranslations, useFormatters } from "@/lib/i18n/context"; @@ -128,8 +133,16 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) { const [fromDay, setFromDayRaw] = useState(""); const [toDay, setToDayRaw] = useState(""); const [page, setPage] = useState(1); - const [sortBy, setSortBy] = useState("moodLoggedAt"); - const [sortDir, setSortDir] = useState<"asc" | "desc">("desc"); + // Shared column-sort state (moodLoggedAt opens descending). + const { + sortBy, + sortDir, + toggleSort: applySort, + } = useTableSort({ + defaultColumn: "moodLoggedAt", + defaultDir: "desc", + descColumns: MOOD_DESC_COLUMNS, + }); // v1.15.13 — page-scoped multi-select selection (current page ids only). const [selectedIds, setSelectedIds] = useState>(() => new Set()); @@ -201,13 +214,10 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) { clearSelection(); }; + // Compose the page-reset + selection-clear side effects around the + // shared sort toggle. function toggleSort(column: string) { - if (sortBy === column) { - setSortDir((d) => (d === "asc" ? "desc" : "asc")); - } else { - setSortBy(column); - setSortDir(column === "moodLoggedAt" ? "desc" : "asc"); - } + applySort(column); setPage(1); clearSelection(); } diff --git a/src/hooks/use-table-sort.ts b/src/hooks/use-table-sort.ts new file mode 100644 index 000000000..776b33b85 --- /dev/null +++ b/src/hooks/use-table-sort.ts @@ -0,0 +1,51 @@ +"use client"; + +import { useCallback, useState } from "react"; + +export type SortDirection = "asc" | "desc"; + +/** + * Shared column-sort state for the data-management tables (measurements, + * mood, lab history). Lifted from the byte-identical `sortBy` / `sortDir` / + * `toggleSort` triple the measurement + mood lists each carried inline. + * + * `toggleSort(column)` flips the direction when the active column is tapped + * again, otherwise switches to the new column with its default direction: + * a column listed in `descColumns` (typically the date column) opens + * descending, every other column ascending. Callers that need extra side + * effects on a sort change (reset the page, clear a selection) compose them + * around the returned `toggleSort` rather than re-implementing the math. + */ +export function useTableSort({ + defaultColumn, + defaultDir = "desc", + descColumns, +}: { + /** Column the table sorts by on first render. */ + defaultColumn: string; + /** Direction for the initial column. */ + defaultDir?: SortDirection; + /** Columns that open descending when first selected (e.g. the date column). */ + descColumns?: ReadonlySet; +}): { + sortBy: string; + sortDir: SortDirection; + toggleSort: (column: string) => void; +} { + const [sortBy, setSortBy] = useState(defaultColumn); + const [sortDir, setSortDir] = useState(defaultDir); + + const toggleSort = useCallback( + (column: string) => { + if (sortBy === column) { + setSortDir((d) => (d === "asc" ? "desc" : "asc")); + } else { + setSortBy(column); + setSortDir(descColumns?.has(column) ? "desc" : "asc"); + } + }, + [sortBy, descColumns], + ); + + return { sortBy, sortDir, toggleSort }; +}