Files
HealthLog/src/hooks/use-priority-item-dismiss.ts
T
Marc-André Bombeck 66fe69b493 fix(ui): break-words on coach prose, hero/wellness skeleton parity, priority-card pending state
Coach chat bubbles and the document OCR text panel wrapped long unbroken
tokens (URLs, IDs) past their column width on narrow viewports — add
break-words. Drop the /90 alpha on the coach error text so it stays on the
AA-retuned status token.

Today hero's loading skeleton had drifted from the v1.29.1 tightened hero
(gap/items-start/space-y/rail-heading/xl:grid-cols-3), causing a visible
shift on every dashboard load; mirror the loaded classes 1:1.

The wellness-scores strip returned null while its shared batch loaded,
so it popped in late and pushed the page below it down — render the
section heading + tile-shaped skeletons instead, matching the vitals
dashboard's loading pattern.

Priority-card actions (coach check-in keep/let-go) had no pending state,
so a slow round trip could be double-tapped into firing the mutation
twice; disable the action row while in flight. Priority-item dismiss is
now optimistic (removes the card from the rail on tap, rolls back on
error) instead of waiting on invalidate-after-success.
2026-07-18 03:52:32 +02:00

55 lines
1.9 KiB
TypeScript

"use client";
/**
* Today rail — "dismiss / mark seen" for the OBSERVATIONAL `PriorityItem`
* kinds (`milestone`, `ecg_new_recording`, `tension_window`). Posts the
* item's own `itemKey` to `POST /api/daily/digest/dismiss` (server persists
* it so the dismissal survives reload / a second device). Keys route
* through the centralised factory — no bare arrays.
*
* The dismiss is optimistic (`onMutate` strikes the item from the cached
* digest immediately): a second tap on the same card has nothing left to
* hit, since the card itself leaves the rail before the round trip lands.
* `onError` rolls the cache back to the pre-tap snapshot; `onSettled`
* re-syncs from the server either way.
*/
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiPost } from "@/lib/api/api-fetch";
import { queryKeys } from "@/lib/query-keys";
import type { DailyDigest } from "@/lib/daily/digest";
export function usePriorityItemDismiss() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (itemKey: string) => {
await apiPost("/api/daily/digest/dismiss", { itemKey });
return itemKey;
},
onMutate: async (itemKey) => {
await queryClient.cancelQueries({ queryKey: queryKeys.dailyDigest() });
const previous = queryClient.getQueryData<DailyDigest>(
queryKeys.dailyDigest(),
);
if (previous) {
queryClient.setQueryData<DailyDigest>(queryKeys.dailyDigest(), {
...previous,
worthALook: previous.worthALook.filter(
(item) => item.itemKey !== itemKey,
),
});
}
return { previous };
},
onError: (_err, _itemKey, context) => {
if (context?.previous) {
queryClient.setQueryData(queryKeys.dailyDigest(), context.previous);
}
},
onSettled: () => {
void queryClient.invalidateQueries({ queryKey: queryKeys.dailyDigest() });
},
});
}