mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 14:16:12 +00:00
Cmd/Ctrl+K or the search control at the end of the top bar opens a combobox over a client-side index built from the lists the navigation already renders: pages, Insights pages, every Settings section and card (deep-linked by a new anchor prop on SettingsCardHeader), the Appearance sub-pages, module switches and a few actions. Entries follow the module map, the Coach capability, the admin role and a shared record's grant; inside somebody else's record no cards, switches or backup are offered. Ranking is prefix, then word start, then substring, then a small edit distance, then subsequence, with a short multilingual synonym list. The empty state shows recent places (per account, in localStorage) and the main actions. The palette is its own lazily loaded chunk; the top bar shows a search field where it has room and a magnifier otherwise. The module switch mutation moves into a shared hook used by the Modules hub and the palette.
112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
"use client";
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { Droplet, Info } from "lucide-react";
|
|
|
|
import { Label } from "@/components/ui/label";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { SettingsCard } from "@/components/settings/settings-card";
|
|
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
|
import { useTranslations } from "@/lib/i18n/context";
|
|
import { queryKeys } from "@/lib/query-keys";
|
|
import { apiGet, apiPatch } from "@/lib/api/api-fetch";
|
|
|
|
/**
|
|
* Glucose reference band — the web control for the declared diabetes opt-in
|
|
* (`GET/PATCH /api/auth/me/diabetes`).
|
|
*
|
|
* WHY IT LIVES ON TARGETS
|
|
* -----------------------
|
|
* The flag answers exactly the question this page answers — "which range is my
|
|
* reading judged against" — and the page already owns the four glucose target
|
|
* rows. Putting it anywhere else would split one concept across two surfaces.
|
|
* The card sits ABOVE the per-metric editor because the two controls are
|
|
* ordered: this one picks the default band, a per-metric override below then
|
|
* beats it (`src/lib/targets/glucose-targets.ts`).
|
|
*
|
|
* WHAT IT DOES NOT DO
|
|
* -------------------
|
|
* The flag is a display preference, never a diagnosis, and is never inferred
|
|
* from a reading. It selects the band the glucose surfaces and the Coach's
|
|
* reference grounding read; the app takes no clinical action on it. The copy
|
|
* says so at the point of the switch rather than in a help page.
|
|
*
|
|
* Note: `GET /api/user/thresholds` (the editor below) reports the general
|
|
* bands regardless of this flag — the flag resolves in the targets pipeline
|
|
* (`src/lib/targets/build-response.ts`), which is why the mutation invalidates
|
|
* `insightsTargets()` rather than `userThresholds()`.
|
|
*/
|
|
|
|
interface DiabetesPrefShape {
|
|
hasDiabetes: boolean;
|
|
}
|
|
|
|
export function GlucoseReferenceCard() {
|
|
const { t } = useTranslations();
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data } = useQuery({
|
|
queryKey: queryKeys.diabetesPref(),
|
|
queryFn: async () => apiGet<DiabetesPrefShape>("/api/auth/me/diabetes"),
|
|
});
|
|
|
|
const toggle = useMutation({
|
|
mutationFn: async (hasDiabetes: boolean) =>
|
|
apiPatch<DiabetesPrefShape>("/api/auth/me/diabetes", { hasDiabetes }),
|
|
onSuccess: (next) => {
|
|
queryClient.setQueryData(queryKeys.diabetesPref(), next);
|
|
void queryClient.invalidateQueries({
|
|
queryKey: queryKeys.insightsTargets(),
|
|
});
|
|
},
|
|
});
|
|
|
|
const enabled = data?.hasDiabetes ?? false;
|
|
|
|
return (
|
|
<SettingsCard>
|
|
<SettingsCardHeader
|
|
anchor="glucose-reference"
|
|
icon={Droplet}
|
|
title={t("settings.glucoseReference.title")}
|
|
description={t("settings.glucoseReference.description")}
|
|
/>
|
|
<div className="space-y-4">
|
|
<div className="flex items-start justify-between gap-4">
|
|
<Label htmlFor="glucose-reference-diabetes" className="text-sm">
|
|
{t("settings.glucoseReference.enable")}
|
|
</Label>
|
|
<Switch
|
|
id="glucose-reference-diabetes"
|
|
checked={enabled}
|
|
disabled={toggle.isPending || data == null}
|
|
onCheckedChange={(v) => toggle.mutate(v)}
|
|
className="mt-0.5 shrink-0"
|
|
/>
|
|
</div>
|
|
|
|
<p className="text-sm">{t("settings.glucoseReference.explainer")}</p>
|
|
|
|
<div
|
|
data-slot="glucose-reference-disclaimer"
|
|
className="border-border bg-muted/40 flex items-start gap-2.5 rounded-lg border-l-2 p-3"
|
|
>
|
|
<Info
|
|
className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0"
|
|
aria-hidden="true"
|
|
/>
|
|
<p className="text-foreground/80 text-xs leading-relaxed">
|
|
{t("settings.glucoseReference.disclaimer")}
|
|
</p>
|
|
</div>
|
|
|
|
{toggle.isError ? (
|
|
<p className="text-destructive text-sm" role="alert">
|
|
{t("settings.glucoseReference.error")}
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
</SettingsCard>
|
|
);
|
|
}
|