mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
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.
91 lines
2.9 KiB
TypeScript
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>
|
|
);
|
|
}
|