Files
HealthLog/src/components/settings/injection-sites-card.tsx
T
Marc-André Bombeck eb730bf7f3 fix(insights): one card language on the sub-page spine
A measured pass over every insights sub-page, the dashboard, and the
settings sections (computed text colour, left text edge relative to the
card edge, header→body distance, per card) surfaced the drift the eye
kept catching:

- The blood-pressure pulse-pressure/MAP explainer was a bare muted
  paragraph at the container edge, 16–24 px left of every neighbouring
  card's text. It now rides a compact Card in the regular foreground.
- The 'Usual range' read strip was a hand-rolled bg-card/60 shell with
  its own padding; rebuilt on Card + TileHeader + CardContent at the
  compact density so background, radius, icon spacing and text edge
  match the neighbouring tiles.
- The spine tiles carried three different header→body distances (6, 12
  and 24 px) via off-scale overrides (gap-1.5, md:gap-3, md:py-5,
  gap-3). Assessment, target summary, medication tiles, the mood
  stand-out card and the sleep hypnogram now share the sanctioned
  compact tier (gap-2 py-3 md:py-4).
- The hero score card and its skeleton kept their translucent identity
  but move from a static px-4 py-4 to the p-4 md:p-6 card norm, putting
  their text on the same edge as every other card.
- Two dashboard shells (recent workouts, recent unlocks) froze at p-4
  on every breakpoint; now p-4 md:p-6.
- Settings: the modules hint moves into the header's description slot
  and the injection-site grid joins the section-wide mt-4 rhythm — the
  two gaps that deviated from the settings 16 px header→content norm.
2026-07-05 22:00:53 +02:00

91 lines
2.9 KiB
TypeScript

"use client";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Syringe } from "lucide-react";
import { SettingsCard } from "@/components/settings/settings-card";
import { SettingsCardHeader } from "@/components/settings/_card-header";
import { Checkbox } from "@/components/ui/checkbox";
import { useTranslations } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import {
INJECTION_SITE_KEYS,
describeInjectionSite,
type InjectionSiteKey,
} from "@/lib/medications/injection-sites";
import { apiFetchRaw, apiPatch } from "@/lib/api/api-fetch";
/**
* v1.8.5 — Settings → global injection-site exclusion.
*
* A per-user deny-list. Sites checked here are never offered for any
* medication's injection-site picker and are rejected (422) at intake —
* even when a medication lists them as preferred (deny always wins).
* Reads + PATCHes `GET/PATCH /api/auth/me/injection-site-prefs`; the
* mutation invalidates `injectionSitePrefs()` so every picker re-reads.
*/
export function InjectionSitesCard({
isAuthenticated,
}: {
isAuthenticated: boolean;
}) {
const { t } = useTranslations();
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: queryKeys.injectionSitePrefs(),
queryFn: async () => {
const res = await apiFetchRaw("/api/auth/me/injection-site-prefs");
if (!res.ok) return [] as InjectionSiteKey[];
const json = await res.json();
return (json.data?.globalExcludedInjectionSites ??
[]) as InjectionSiteKey[];
},
enabled: isAuthenticated,
staleTime: 5 * 60 * 1000,
});
const excluded: InjectionSiteKey[] = data ?? [];
const mutation = useMutation({
mutationFn: async (next: InjectionSiteKey[]) => {
await apiPatch("/api/auth/me/injection-site-prefs", {
globalExcludedInjectionSites: next,
});
},
onSuccess: () => {
void queryClient.invalidateQueries({
queryKey: queryKeys.injectionSitePrefs(),
});
},
});
function toggle(site: InjectionSiteKey, checked: boolean) {
const set = new Set(excluded);
if (checked) set.add(site);
else set.delete(site);
mutation.mutate(INJECTION_SITE_KEYS.filter((s) => set.has(s)));
}
return (
<SettingsCard>
<SettingsCardHeader
icon={Syringe}
title={t("settings.globalExcludedInjectionSitesLabel")}
description={t("settings.globalExcludedInjectionSitesHint")}
/>
<div className="mt-4 grid grid-cols-1 gap-2 pl-7 sm:grid-cols-2">
{INJECTION_SITE_KEYS.map((site) => (
<label key={site} className="flex items-center gap-2 text-sm">
<Checkbox
checked={excluded.includes(site)}
disabled={mutation.isPending}
onCheckedChange={(c) => toggle(site, c === true)}
/>
{t(describeInjectionSite(site))}
</label>
))}
</div>
</SettingsCard>
);
}