mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
fix(custom-metrics,medications): shared error cards, client nav, themed delete
- CustomMetricList load error uses QueryErrorCard with retry; post-create navigation switches from a full-page window.location.assign to router.push. - CustomMetricDetail + medications list swap their hand-rolled error blocks for the shared QueryErrorCard. - Side-effect delete now uses the themed AlertDialog confirm instead of a native window.confirm.
This commit is contained in:
1 parent
930fa81124
commit
188662311a
10 files changed
+91
-53
No files matched your search
+3
-1
@@ -955,7 +955,9 @@
|
||||
"gastroparesisLike": "Verlangsamte Magenentleerung",
|
||||
"dysgeusia": "Geschmacksveränderung",
|
||||
"gallbladderDiscomfort": "Gallenblasen-Beschwerden"
|
||||
}
|
||||
},
|
||||
"deleteConfirmTitle": "Eintrag löschen?",
|
||||
"deleteConfirmBody": "Dieser Nebenwirkungs-Eintrag wird dauerhaft entfernt."
|
||||
},
|
||||
"scheduling": {
|
||||
"naturalLanguage": {
|
||||
|
||||
+3
-1
@@ -955,7 +955,9 @@
|
||||
"gastroparesisLike": "Slow stomach emptying",
|
||||
"dysgeusia": "Taste change",
|
||||
"gallbladderDiscomfort": "Gallbladder discomfort"
|
||||
}
|
||||
},
|
||||
"deleteConfirmTitle": "Delete this entry?",
|
||||
"deleteConfirmBody": "This side-effect entry will be permanently removed."
|
||||
},
|
||||
"scheduling": {
|
||||
"naturalLanguage": {
|
||||
|
||||
+3
-1
@@ -955,7 +955,9 @@
|
||||
"gastroparesisLike": "Vaciado gástrico lento",
|
||||
"dysgeusia": "Alteración del gusto",
|
||||
"gallbladderDiscomfort": "Molestia en la vesícula biliar"
|
||||
}
|
||||
},
|
||||
"deleteConfirmTitle": "¿Eliminar esta entrada?",
|
||||
"deleteConfirmBody": "Esta entrada de efecto secundario se eliminará de forma permanente."
|
||||
},
|
||||
"scheduling": {
|
||||
"naturalLanguage": {
|
||||
|
||||
+3
-1
@@ -955,7 +955,9 @@
|
||||
"gastroparesisLike": "Vidange gastrique lente",
|
||||
"dysgeusia": "Altération du goût",
|
||||
"gallbladderDiscomfort": "Inconfort de la vésicule biliaire"
|
||||
}
|
||||
},
|
||||
"deleteConfirmTitle": "Supprimer cette entrée ?",
|
||||
"deleteConfirmBody": "Cette entrée d’effet secondaire sera définitivement supprimée."
|
||||
},
|
||||
"scheduling": {
|
||||
"naturalLanguage": {
|
||||
|
||||
+3
-1
@@ -955,7 +955,9 @@
|
||||
"gastroparesisLike": "Svuotamento gastrico lento",
|
||||
"dysgeusia": "Alterazione del gusto",
|
||||
"gallbladderDiscomfort": "Fastidio alla cistifellea"
|
||||
}
|
||||
},
|
||||
"deleteConfirmTitle": "Eliminare questa voce?",
|
||||
"deleteConfirmBody": "Questa voce di effetto collaterale verrà rimossa definitivamente."
|
||||
},
|
||||
"scheduling": {
|
||||
"naturalLanguage": {
|
||||
|
||||
+3
-1
@@ -955,7 +955,9 @@
|
||||
"gastroparesisLike": "Spowolnione opróżnianie żołądka",
|
||||
"dysgeusia": "Zmiana smaku",
|
||||
"gallbladderDiscomfort": "Dolegliwości pęcherzyka żółciowego"
|
||||
}
|
||||
},
|
||||
"deleteConfirmTitle": "Usunąć ten wpis?",
|
||||
"deleteConfirmBody": "Ten wpis o skutku ubocznym zostanie trwale usunięty."
|
||||
},
|
||||
"scheduling": {
|
||||
"naturalLanguage": {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
CheckCheck,
|
||||
@@ -426,20 +427,11 @@ export default function MedicationsPage() {
|
||||
)}
|
||||
</div>
|
||||
) : isError ? (
|
||||
<div className="bg-card border-border flex h-64 items-center justify-center rounded-xl border">
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("medications.loadFailed")}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void refetchMedications()}
|
||||
>
|
||||
{t("medications.retryLoad")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<QueryErrorCard
|
||||
description={t("medications.loadFailed")}
|
||||
retryLabel={t("medications.retryLoad")}
|
||||
onRetry={() => void refetchMedications()}
|
||||
/>
|
||||
) : !medications?.length ? (
|
||||
// v1.4.15 phase-C5: refactor the inline icon+text+button block
|
||||
// to the shared EmptyState primitive so the empty path matches
|
||||
|
||||
@@ -15,6 +15,7 @@ import { toast } from "sonner";
|
||||
import { DeleteButton } from "@/components/data-list";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -178,23 +179,13 @@ export function CustomMetricDetail({
|
||||
{description}
|
||||
</p>
|
||||
</header>
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center gap-3 py-8 text-center">
|
||||
<p className="text-destructive text-sm">
|
||||
{t("customMetrics.loadError")}
|
||||
</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
if (metricError) void refetchMetric();
|
||||
if (listError) void refetchList();
|
||||
}}
|
||||
>
|
||||
{t("common.retry")}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<QueryErrorCard
|
||||
description={t("customMetrics.loadError")}
|
||||
onRetry={() => {
|
||||
if (metricError) void refetchMetric();
|
||||
if (listError) void refetchList();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { ChevronRight, Gauge, Plus } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
@@ -27,10 +29,11 @@ import type { CustomMetricDto, CustomMetricListResponse } from "./types";
|
||||
*/
|
||||
export function CustomMetricList() {
|
||||
const { t } = useTranslations();
|
||||
const router = useRouter();
|
||||
const [addOpen, setAddOpen] = useState(false);
|
||||
const [addFooterEl, setAddFooterEl] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: queryKeys.customMetrics(),
|
||||
queryFn: () => apiGet<CustomMetricListResponse>("/api/custom-metrics"),
|
||||
});
|
||||
@@ -40,10 +43,8 @@ export function CustomMetricList() {
|
||||
function afterAdd(saved: CustomMetricDto) {
|
||||
setAddOpen(false);
|
||||
// Land the user on the new metric's detail page so the next step (logging
|
||||
// a value) is one tap away.
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.assign(`/custom-metrics/${saved.id}`);
|
||||
}
|
||||
// a value) is one tap away — client nav, not a full-page reload.
|
||||
router.push(`/custom-metrics/${saved.id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -80,9 +81,10 @@ export function CustomMetricList() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : isError ? (
|
||||
<p role="alert" className="text-destructive py-4 text-center text-sm">
|
||||
{t("customMetrics.loadError")}
|
||||
</p>
|
||||
<QueryErrorCard
|
||||
description={t("customMetrics.loadError")}
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
) : metrics.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="flex flex-col items-center gap-3 py-6 text-center">
|
||||
|
||||
@@ -8,6 +8,16 @@ import { Loader2, Plus, Trash2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
@@ -113,6 +123,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
// into the `<ResponsiveSheet>` footer slot. The submit button keeps
|
||||
// its `<form>` association via the HTML `form` attribute.
|
||||
const [footerEl, setFooterEl] = useState<HTMLDivElement | null>(null);
|
||||
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
|
||||
const formId = useId();
|
||||
|
||||
const listKey = queryKeys.medicationSideEffects(medicationId);
|
||||
@@ -330,14 +341,7 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-destructive size-11 shrink-0"
|
||||
aria-label={t("medications.sideEffects.deleteCta")}
|
||||
onClick={() => {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
window.confirm(t("medications.sideEffects.deleteConfirm"))
|
||||
) {
|
||||
deleteMutation.mutate(row.id);
|
||||
}
|
||||
}}
|
||||
onClick={() => setPendingDeleteId(row.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
@@ -348,6 +352,43 @@ export function SideEffectsSection({ medicationId }: SideEffectsSectionProps) {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Themed confirm — replaces the native window.confirm so the delete
|
||||
gate matches every other destructive action in the app. */}
|
||||
<AlertDialog
|
||||
open={!!pendingDeleteId}
|
||||
onOpenChange={(o) => {
|
||||
if (!o) setPendingDeleteId(null);
|
||||
}}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
{t("medications.sideEffects.deleteConfirmTitle")}
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{t("medications.sideEffects.deleteConfirmBody")}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={deleteMutation.isPending}>
|
||||
{t("common.cancel")}
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (pendingDeleteId) deleteMutation.mutate(pendingDeleteId);
|
||||
setPendingDeleteId(null);
|
||||
}}
|
||||
disabled={deleteMutation.isPending}
|
||||
aria-busy={deleteMutation.isPending || undefined}
|
||||
className="bg-destructive dark:bg-destructive/60 text-white hover:bg-destructive/90"
|
||||
>
|
||||
{t("medications.sideEffects.deleteCta")}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<ResponsiveSheet
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
|
||||
Reference in new issue
Block a user