mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
chore(style): repo-wide prettier sweep before v1.4.6 tag
Long-overdue prettier sweep; prettier had drifted vs. checked-in code across ~130 files (mostly long-argument line-break wrapping in TS and table-padding in markdown). Pure formatting — no semantic changes — and required to clear the v1.4.6 release gate's `pnpm format:check`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
67575180b8
commit
02b99553b9
135 files changed
+2159
-1583
No files matched your search
@@ -206,8 +206,7 @@ function FeedbackCategoryBadge({
|
||||
> = {
|
||||
BUG: {
|
||||
label: t("admin.feedback.categoryBug"),
|
||||
className:
|
||||
"bg-dracula-red/15 text-dracula-red border-dracula-red/30",
|
||||
className: "bg-dracula-red/15 text-dracula-red border-dracula-red/30",
|
||||
},
|
||||
FEATURE_REQUEST: {
|
||||
label: t("admin.feedback.categoryFeature"),
|
||||
@@ -216,8 +215,7 @@ function FeedbackCategoryBadge({
|
||||
},
|
||||
QUESTION: {
|
||||
label: t("admin.feedback.categoryQuestion"),
|
||||
className:
|
||||
"bg-dracula-cyan/15 text-dracula-cyan border-dracula-cyan/30",
|
||||
className: "bg-dracula-cyan/15 text-dracula-cyan border-dracula-cyan/30",
|
||||
},
|
||||
OTHER: {
|
||||
label: t("admin.feedback.categoryOther"),
|
||||
|
||||
@@ -76,7 +76,13 @@ export function ComplianceHeatmap({
|
||||
} | null>(null);
|
||||
|
||||
const WEEKDAY_LABELS = [
|
||||
t("charts.weekdays.mon"), "", t("charts.weekdays.wed"), "", t("charts.weekdays.fri"), "", t("charts.weekdays.sun"),
|
||||
t("charts.weekdays.mon"),
|
||||
"",
|
||||
t("charts.weekdays.wed"),
|
||||
"",
|
||||
t("charts.weekdays.fri"),
|
||||
"",
|
||||
t("charts.weekdays.sun"),
|
||||
];
|
||||
useEffect(() => {
|
||||
if (!stretch) return;
|
||||
@@ -98,10 +104,18 @@ export function ComplianceHeatmap({
|
||||
|
||||
const { cells, weeks, monthMarkers } = useMemo(() => {
|
||||
const MONTH_LABELS = [
|
||||
t("charts.months.jan"), t("charts.months.feb"), t("charts.months.mar"),
|
||||
t("charts.months.apr"), t("charts.months.may"), t("charts.months.jun"),
|
||||
t("charts.months.jul"), t("charts.months.aug"), t("charts.months.sep"),
|
||||
t("charts.months.oct"), t("charts.months.nov"), t("charts.months.dec"),
|
||||
t("charts.months.jan"),
|
||||
t("charts.months.feb"),
|
||||
t("charts.months.mar"),
|
||||
t("charts.months.apr"),
|
||||
t("charts.months.may"),
|
||||
t("charts.months.jun"),
|
||||
t("charts.months.jul"),
|
||||
t("charts.months.aug"),
|
||||
t("charts.months.sep"),
|
||||
t("charts.months.oct"),
|
||||
t("charts.months.nov"),
|
||||
t("charts.months.dec"),
|
||||
];
|
||||
const now = new Date();
|
||||
const cellList: Array<{
|
||||
|
||||
@@ -17,12 +17,7 @@ import { useState, useMemo, useId } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
CardContent,
|
||||
} from "@/components/ui/card";
|
||||
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
|
||||
import { formatDateShort } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { DataSummary } from "@/lib/analytics/trends";
|
||||
@@ -50,10 +45,26 @@ interface MoodChartProps {
|
||||
// --- Constants ---
|
||||
|
||||
const TIME_RANGES_KEYS = [
|
||||
{ labelKey: "charts.points7Label", points: 7, titleKey: "charts.points7Title" },
|
||||
{ labelKey: "charts.points30Label", points: 30, titleKey: "charts.points30Title" },
|
||||
{ labelKey: "charts.points90Label", points: 90, titleKey: "charts.points90Title" },
|
||||
{ labelKey: "charts.pointsAllLabel", points: 0, titleKey: "charts.pointsAllTitle" },
|
||||
{
|
||||
labelKey: "charts.points7Label",
|
||||
points: 7,
|
||||
titleKey: "charts.points7Title",
|
||||
},
|
||||
{
|
||||
labelKey: "charts.points30Label",
|
||||
points: 30,
|
||||
titleKey: "charts.points30Title",
|
||||
},
|
||||
{
|
||||
labelKey: "charts.points90Label",
|
||||
points: 90,
|
||||
titleKey: "charts.points90Title",
|
||||
},
|
||||
{
|
||||
labelKey: "charts.pointsAllLabel",
|
||||
points: 0,
|
||||
titleKey: "charts.pointsAllTitle",
|
||||
},
|
||||
] as const;
|
||||
|
||||
// MOOD_LABELS built dynamically via t() in the component
|
||||
@@ -95,9 +106,10 @@ function movingAverageByPoints(
|
||||
});
|
||||
}
|
||||
|
||||
function buildTrendLine(
|
||||
data: Array<{ date: Date; value: number }>,
|
||||
): { points: Array<{ date: Date; value: number }>; slopePerDay: number } | null {
|
||||
function buildTrendLine(data: Array<{ date: Date; value: number }>): {
|
||||
points: Array<{ date: Date; value: number }>;
|
||||
slopePerDay: number;
|
||||
} | null {
|
||||
if (data.length < 2) return null;
|
||||
|
||||
const sorted = [...data].sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
@@ -188,9 +200,7 @@ export function MoodChart({ title }: MoodChartProps) {
|
||||
const ma = movingAverageByPoints(scoreData, 7);
|
||||
for (const point of ma) {
|
||||
const pointTimestamp = point.date.getTime();
|
||||
const existing = enriched.find(
|
||||
(d) => d.timestamp === pointTimestamp,
|
||||
);
|
||||
const existing = enriched.find((d) => d.timestamp === pointTimestamp);
|
||||
if (existing) {
|
||||
existing.ma = point.value;
|
||||
}
|
||||
|
||||
@@ -88,9 +88,7 @@ export function TrendCard({
|
||||
</div>
|
||||
<div className="mt-2 flex items-baseline gap-2">
|
||||
<span className="text-3xl font-bold tracking-tight tabular-nums">
|
||||
{latest !== null
|
||||
? renderPair(latest, secondary?.latest)
|
||||
: "—"}
|
||||
{latest !== null ? renderPair(latest, secondary?.latest) : "—"}
|
||||
</span>
|
||||
<span className="text-muted-foreground text-sm tabular-nums">
|
||||
{unit}
|
||||
@@ -115,9 +113,7 @@ export function TrendCard({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className={cn("font-medium tabular-nums", avg7ColorClass)}
|
||||
>
|
||||
<span className={cn("font-medium tabular-nums", avg7ColorClass)}>
|
||||
{avg7 !== null ? renderPair(avg7, secondary?.avg7) : "—"}
|
||||
</span>
|
||||
)}
|
||||
@@ -138,9 +134,7 @@ export function TrendCard({
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<span
|
||||
className={cn("font-medium tabular-nums", avg30ColorClass)}
|
||||
>
|
||||
<span className={cn("font-medium tabular-nums", avg30ColorClass)}>
|
||||
{avg30 !== null ? renderPair(avg30, secondary?.avg30) : "—"}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -39,8 +39,7 @@ export function ErrorDetails({
|
||||
// in the query string (Withings connect, password reset) and we don't
|
||||
// want users pasting those into public GitHub issues. Preserve the count
|
||||
// for context without exposing values.
|
||||
const loc =
|
||||
typeof window !== "undefined" ? window.location : null;
|
||||
const loc = typeof window !== "undefined" ? window.location : null;
|
||||
const urlPath = loc?.pathname ?? null;
|
||||
const searchParamCount = loc?.search
|
||||
? new URLSearchParams(loc.search).toString().split("&").filter(Boolean)
|
||||
@@ -55,8 +54,7 @@ export function ErrorDetails({
|
||||
requestId: requestId ?? null,
|
||||
urlPath,
|
||||
searchParamCount,
|
||||
userAgent:
|
||||
typeof navigator !== "undefined" ? navigator.userAgent : null,
|
||||
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
|
||||
locale,
|
||||
timestamp: new Date().toISOString(),
|
||||
context,
|
||||
@@ -79,7 +77,7 @@ export function ErrorDetails({
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("errorBoundary.description")}
|
||||
</p>
|
||||
<div className="bg-muted/50 rounded-md border p-3 font-mono text-xs text-muted-foreground break-all">
|
||||
<div className="bg-muted/50 text-muted-foreground rounded-md border p-3 font-mono text-xs break-all">
|
||||
{error.message || t("common.unknownError")}
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -91,7 +89,7 @@ export function ErrorDetails({
|
||||
)}
|
||||
<Button onClick={handleCopy} variant="outline" size="sm">
|
||||
{copied ? (
|
||||
<CheckCircle2 className="mr-2 h-4 w-4 text-dracula-green" />
|
||||
<CheckCircle2 className="text-dracula-green mr-2 h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
|
||||
@@ -33,8 +33,8 @@ export function InsightStatusCard({
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-dracula-purple" />
|
||||
<span className="ml-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="text-dracula-purple h-5 w-5 animate-spin" />
|
||||
<span className="text-muted-foreground ml-2 text-sm">
|
||||
{t("common.loading")}
|
||||
</span>
|
||||
</CardContent>
|
||||
@@ -52,7 +52,7 @@ export function InsightStatusCard({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.noProviderConfigured")}
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -70,7 +70,7 @@ export function InsightStatusCard({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.noAnalysisYet")}
|
||||
</p>
|
||||
</CardContent>
|
||||
@@ -79,7 +79,7 @@ export function InsightStatusCard({
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="animate-insight-in border-l-2 border-l-dracula-purple">
|
||||
<Card className="animate-insight-in border-l-dracula-purple border-l-2">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -87,14 +87,14 @@ export function InsightStatusCard({
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
</div>
|
||||
{cached && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{t("insights.cached")}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2">
|
||||
<p className="text-sm leading-relaxed text-muted-foreground">{text}</p>
|
||||
<p className="text-muted-foreground text-sm leading-relaxed">{text}</p>
|
||||
<LastUpdatedFooter updatedAt={updatedAt} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -106,7 +106,7 @@ function LastUpdatedFooter({ updatedAt }: { updatedAt: string | null }) {
|
||||
const fmt = useFormatters();
|
||||
if (!updatedAt) return null;
|
||||
return (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("insights.lastUpdated")}: {fmt.dateTime(updatedAt)}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -74,7 +74,8 @@ export function InsightsCard() {
|
||||
});
|
||||
|
||||
// Don't show if no API key configured
|
||||
if (!(settings?.codexStatus === "connected" || settings?.hasAdminKey)) return null;
|
||||
if (!(settings?.codexStatus === "connected" || settings?.hasAdminKey))
|
||||
return null;
|
||||
|
||||
const insights = generate.data?.insights;
|
||||
|
||||
@@ -84,7 +85,9 @@ export function InsightsCard() {
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Sparkles className="text-dracula-purple h-5 w-5" />
|
||||
<CardTitle className="text-lg">{t("insights.aiInsights")}</CardTitle>
|
||||
<CardTitle className="text-lg">
|
||||
{t("insights.aiInsights")}
|
||||
</CardTitle>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
@@ -97,7 +100,9 @@ export function InsightsCard() {
|
||||
) : (
|
||||
<RefreshCw className="mr-1 h-3.5 w-3.5" />
|
||||
)}
|
||||
{insights ? t("insights.refreshButton") : t("insights.generateButton")}
|
||||
{insights
|
||||
? t("insights.refreshButton")
|
||||
: t("insights.generateButton")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -123,7 +128,8 @@ export function InsightsCard() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={CONFIDENCE_COLORS[insights.confidence] ?? ""}>
|
||||
{t("insights.confidence")}: {t(`insights.confidence_${insights.confidence}`)}
|
||||
{t("insights.confidence")}:{" "}
|
||||
{t(`insights.confidence_${insights.confidence}`)}
|
||||
</Badge>
|
||||
{generate.data?.cached && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
@@ -132,10 +138,22 @@ export function InsightsCard() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Section title={t("insights.sectionChanged")} text={insights.changed} />
|
||||
<Section title={t("insights.sectionStable")} text={insights.stable} />
|
||||
<Section title={t("insights.sectionDrivers")} text={insights.drivers} />
|
||||
<Section title={t("insights.sectionNextSteps")} text={insights.nextSteps} />
|
||||
<Section
|
||||
title={t("insights.sectionChanged")}
|
||||
text={insights.changed}
|
||||
/>
|
||||
<Section
|
||||
title={t("insights.sectionStable")}
|
||||
text={insights.stable}
|
||||
/>
|
||||
<Section
|
||||
title={t("insights.sectionDrivers")}
|
||||
text={insights.drivers}
|
||||
/>
|
||||
<Section
|
||||
title={t("insights.sectionNextSteps")}
|
||||
text={insights.nextSteps}
|
||||
/>
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-3">
|
||||
<p className="text-muted-foreground text-xs">
|
||||
|
||||
@@ -116,7 +116,7 @@ export function AuthShell({ children }: { children: React.ReactNode }) {
|
||||
) : null}
|
||||
<a
|
||||
href="#main-content"
|
||||
className="bg-primary text-primary-foreground fixed left-2 top-2 z-[100] rounded-md px-4 py-2 text-sm font-medium opacity-0 focus:opacity-100"
|
||||
className="bg-primary text-primary-foreground fixed top-2 left-2 z-[100] rounded-md px-4 py-2 text-sm font-medium opacity-0 focus:opacity-100"
|
||||
>
|
||||
{t("nav.skipToContent")}
|
||||
</a>
|
||||
|
||||
@@ -545,7 +545,11 @@ export function MeasurementList({ onEdit }: MeasurementListProps) {
|
||||
</div>
|
||||
|
||||
{editError && (
|
||||
<div role="alert" aria-live="assertive" className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm">
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
|
||||
>
|
||||
{editError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -88,7 +88,11 @@ function dateTimeLocalToISO(value: string): string {
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
|
||||
export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange }: IntakeHistoryListProps) {
|
||||
export function IntakeHistoryList({
|
||||
medicationId,
|
||||
createOpen,
|
||||
onCreateOpenChange,
|
||||
}: IntakeHistoryListProps) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslations();
|
||||
@@ -120,16 +124,19 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
const [createSkipped, setCreateSkipped] = useState(false);
|
||||
const [createError, setCreateError] = useState<string | null>(null);
|
||||
const setCreatingRaw = onCreateOpenChange ?? setInternalCreating;
|
||||
const setCreating = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
const now = toDateTimeLocalValue(new Date().toISOString());
|
||||
setCreateScheduledFor(now);
|
||||
setCreateTakenAt(now);
|
||||
setCreateSkipped(false);
|
||||
setCreateError(null);
|
||||
}
|
||||
setCreatingRaw(open);
|
||||
}, [setCreatingRaw]);
|
||||
const setCreating = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) {
|
||||
const now = toDateTimeLocalValue(new Date().toISOString());
|
||||
setCreateScheduledFor(now);
|
||||
setCreateTakenAt(now);
|
||||
setCreateSkipped(false);
|
||||
setCreateError(null);
|
||||
}
|
||||
setCreatingRaw(open);
|
||||
},
|
||||
[setCreatingRaw],
|
||||
);
|
||||
|
||||
function toggleSort(column: string) {
|
||||
if (sortBy === column) {
|
||||
@@ -142,14 +149,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
}
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: [
|
||||
"medications",
|
||||
medicationId,
|
||||
"intake",
|
||||
page,
|
||||
sortBy,
|
||||
sortDir,
|
||||
],
|
||||
queryKey: ["medications", medicationId, "intake", page, sortBy, sortDir],
|
||||
queryFn: async () => {
|
||||
const params = new URLSearchParams();
|
||||
params.set("limit", String(PAGE_SIZE));
|
||||
@@ -227,14 +227,11 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
takenAt: string | null;
|
||||
skipped: boolean;
|
||||
}) => {
|
||||
const res = await fetch(
|
||||
`/api/medications/${medicationId}/intake`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ scheduledFor, takenAt, skipped }),
|
||||
},
|
||||
);
|
||||
const res = await fetch(`/api/medications/${medicationId}/intake`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ scheduledFor, takenAt, skipped }),
|
||||
});
|
||||
const json = await res.json();
|
||||
if (!res.ok) throw new Error(json.error ?? "Create failed");
|
||||
},
|
||||
@@ -267,7 +264,6 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
setEditDeleteDialogOpen(false);
|
||||
}
|
||||
|
||||
|
||||
function closeCreate() {
|
||||
if (createMutation.isPending) return;
|
||||
setCreating(false);
|
||||
@@ -299,9 +295,8 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
updateMutation.mutate({
|
||||
eventId: editing.id,
|
||||
scheduledFor: dateTimeLocalToISO(editScheduledFor),
|
||||
takenAt: editSkipped || !editTakenAt
|
||||
? null
|
||||
: dateTimeLocalToISO(editTakenAt),
|
||||
takenAt:
|
||||
editSkipped || !editTakenAt ? null : dateTimeLocalToISO(editTakenAt),
|
||||
skipped: editSkipped,
|
||||
});
|
||||
}
|
||||
@@ -318,9 +313,10 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
setCreateError(null);
|
||||
createMutation.mutate({
|
||||
scheduledFor: dateTimeLocalToISO(createScheduledFor),
|
||||
takenAt: createSkipped || !createTakenAt
|
||||
? null
|
||||
: dateTimeLocalToISO(createTakenAt),
|
||||
takenAt:
|
||||
createSkipped || !createTakenAt
|
||||
? null
|
||||
: dateTimeLocalToISO(createTakenAt),
|
||||
skipped: createSkipped,
|
||||
});
|
||||
}
|
||||
@@ -336,7 +332,10 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
}
|
||||
if (event.takenAt) {
|
||||
return (
|
||||
<Badge variant="secondary" className="gap-1 bg-green-500/20 text-green-400">
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="gap-1 bg-green-500/20 text-green-400"
|
||||
>
|
||||
<Check className="h-3 w-3" />
|
||||
{t("medications.intakeStatusTaken")}
|
||||
</Badge>
|
||||
@@ -398,7 +397,9 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
currentDir={sortDir}
|
||||
onSort={toggleSort}
|
||||
/>
|
||||
<TableHead className="w-32">{t("medications.intakeStatus")}</TableHead>
|
||||
<TableHead className="w-32">
|
||||
{t("medications.intakeStatus")}
|
||||
</TableHead>
|
||||
<SortableHead
|
||||
column="source"
|
||||
label={t("medications.intakeSource")}
|
||||
@@ -419,9 +420,7 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
{formatDateTime(event.scheduledFor)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm tabular-nums">
|
||||
{event.takenAt
|
||||
? formatDateTime(event.takenAt)
|
||||
: "—"}
|
||||
{event.takenAt ? formatDateTime(event.takenAt) : "—"}
|
||||
</TableCell>
|
||||
<TableCell>{getStatusBadge(event)}</TableCell>
|
||||
<TableCell>
|
||||
@@ -442,7 +441,9 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</Button>
|
||||
<DeleteButton
|
||||
label={t("medications.intakeDeleteConfirm")}
|
||||
description={t("medications.intakeDeleteDescription")}
|
||||
description={t(
|
||||
"medications.intakeDeleteDescription",
|
||||
)}
|
||||
onConfirm={() => deleteMutation.mutate(event.id)}
|
||||
/>
|
||||
</div>
|
||||
@@ -467,7 +468,8 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</p>
|
||||
{event.takenAt && (
|
||||
<p className="text-muted-foreground text-xs tabular-nums">
|
||||
{t("medications.intakeTakenAt")}: {formatDateTime(event.takenAt)}
|
||||
{t("medications.intakeTakenAt")}:{" "}
|
||||
{formatDateTime(event.takenAt)}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -550,7 +552,9 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="edit-takenAt">{t("medications.intakeTakenAt")}</Label>
|
||||
<Label htmlFor="edit-takenAt">
|
||||
{t("medications.intakeTakenAt")}
|
||||
</Label>
|
||||
<Input
|
||||
id="edit-takenAt"
|
||||
type="datetime-local"
|
||||
@@ -575,7 +579,11 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</div>
|
||||
|
||||
{editError && (
|
||||
<div role="alert" aria-live="assertive" className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm">
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
|
||||
>
|
||||
{editError}
|
||||
</div>
|
||||
)}
|
||||
@@ -688,7 +696,9 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="create-takenAt">{t("medications.intakeTakenAt")}</Label>
|
||||
<Label htmlFor="create-takenAt">
|
||||
{t("medications.intakeTakenAt")}
|
||||
</Label>
|
||||
<Input
|
||||
id="create-takenAt"
|
||||
type="datetime-local"
|
||||
@@ -713,7 +723,11 @@ export function IntakeHistoryList({ medicationId, createOpen, onCreateOpenChange
|
||||
</div>
|
||||
|
||||
{createError && (
|
||||
<div role="alert" aria-live="assertive" className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm">
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
|
||||
>
|
||||
{createError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -238,12 +238,15 @@ export function MedicationForm({
|
||||
const fmt = useFormatters();
|
||||
|
||||
const doseUnits = [
|
||||
"mg", "g", "ml",
|
||||
"mg",
|
||||
"g",
|
||||
"ml",
|
||||
t("medications.unitDrops"),
|
||||
t("medications.unitTablets"),
|
||||
t("medications.unitCapsules"),
|
||||
t("medications.unitPieces"),
|
||||
"IE", "µg",
|
||||
"IE",
|
||||
"µg",
|
||||
];
|
||||
|
||||
const [name, setName] = useState(initial?.name ?? "");
|
||||
@@ -739,7 +742,11 @@ export function MedicationForm({
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div role="alert" aria-live="assertive" className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm">
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -162,7 +162,11 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div role="alert" aria-live="assertive" className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm">
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -76,7 +76,6 @@ const MOOD_LABEL_KEYS: Record<string, string> = {
|
||||
LAUSIG: "mood.levelLausig",
|
||||
};
|
||||
|
||||
|
||||
interface MoodEntry {
|
||||
id: string;
|
||||
date: string;
|
||||
@@ -342,7 +341,11 @@ export function MoodList() {
|
||||
<TableCell className="pl-4 font-semibold tabular-nums">
|
||||
{entry.score}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
({MOOD_LABEL_KEYS[entry.mood] ? t(MOOD_LABEL_KEYS[entry.mood]) : entry.mood})
|
||||
(
|
||||
{MOOD_LABEL_KEYS[entry.mood]
|
||||
? t(MOOD_LABEL_KEYS[entry.mood])
|
||||
: entry.mood}
|
||||
)
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm">
|
||||
@@ -388,7 +391,7 @@ export function MoodList() {
|
||||
className="bg-card border-border flex items-center justify-between rounded-lg border p-3"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 overflow-hidden">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted">
|
||||
<div className="bg-muted flex h-8 w-8 shrink-0 items-center justify-center rounded-lg">
|
||||
<span className="text-lg font-bold tabular-nums">
|
||||
{entry.score}
|
||||
</span>
|
||||
@@ -397,7 +400,11 @@ export function MoodList() {
|
||||
<span className="text-sm font-semibold tabular-nums">
|
||||
{entry.score}{" "}
|
||||
<span className="text-muted-foreground font-normal">
|
||||
({MOOD_LABEL_KEYS[entry.mood] ? t(MOOD_LABEL_KEYS[entry.mood]) : entry.mood})
|
||||
(
|
||||
{MOOD_LABEL_KEYS[entry.mood]
|
||||
? t(MOOD_LABEL_KEYS[entry.mood])
|
||||
: entry.mood}
|
||||
)
|
||||
</span>
|
||||
</span>
|
||||
<p className="text-muted-foreground truncate text-xs">
|
||||
@@ -471,7 +478,11 @@ export function MoodList() {
|
||||
<form onSubmit={submitEdit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label id="edit-mood-level-label">{t("mood.moodLevel")}</Label>
|
||||
<div role="radiogroup" aria-labelledby="edit-mood-level-label" className="grid grid-cols-5 gap-2">
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-labelledby="edit-mood-level-label"
|
||||
className="grid grid-cols-5 gap-2"
|
||||
>
|
||||
{MOOD_LEVELS_LIST.map((level) => {
|
||||
const isSelected = editMood === level;
|
||||
return (
|
||||
@@ -530,7 +541,11 @@ export function MoodList() {
|
||||
</div>
|
||||
|
||||
{editError && (
|
||||
<div role="alert" aria-live="assertive" className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm">
|
||||
<div
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
className="bg-destructive/10 text-destructive rounded-lg p-3 text-sm"
|
||||
>
|
||||
{editError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -127,6 +127,10 @@ export function AboutSection() {
|
||||
// Auto-check on mount when the last successful check is older than the
|
||||
// refresh interval (or has never run). Keeps the UI populated without
|
||||
// requiring a click — but won't spam GitHub on every navigation.
|
||||
// The setState inside runCheck() is intentional: the check is the
|
||||
// whole point of the effect, and the staleness gate prevents a
|
||||
// cascading-render loop because the timestamp resets after the run.
|
||||
/* eslint-disable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
|
||||
useEffect(() => {
|
||||
if (!version) return;
|
||||
const last = readLastCheckedISO();
|
||||
@@ -136,8 +140,8 @@ export function AboutSection() {
|
||||
if (stale) {
|
||||
void runCheck();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [version?.version]);
|
||||
/* eslint-enable react-hooks/exhaustive-deps, react-hooks/set-state-in-effect */
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -176,14 +180,14 @@ export function AboutSection() {
|
||||
) : (
|
||||
<dl className="flex flex-wrap items-baseline gap-x-6 gap-y-3 text-sm">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<dt className="text-muted-foreground text-xs uppercase tracking-wide">
|
||||
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
||||
{t("settings.about.version")}
|
||||
</dt>
|
||||
<dd className="font-mono font-medium">v{version.version}</dd>
|
||||
</div>
|
||||
|
||||
<div className="flex items-baseline gap-2">
|
||||
<dt className="text-muted-foreground text-xs uppercase tracking-wide">
|
||||
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
||||
{t("settings.about.license")}
|
||||
</dt>
|
||||
<dd className="font-mono">{version.license}</dd>
|
||||
@@ -191,7 +195,7 @@ export function AboutSection() {
|
||||
|
||||
{version.buildSha && (
|
||||
<div className="flex items-baseline gap-2">
|
||||
<dt className="text-muted-foreground text-xs uppercase tracking-wide">
|
||||
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
||||
{t("settings.about.gitSha")}
|
||||
</dt>
|
||||
<dd className="font-mono">{version.buildSha.slice(0, 7)}</dd>
|
||||
@@ -200,7 +204,7 @@ export function AboutSection() {
|
||||
|
||||
{version.builtAt && (
|
||||
<div className="flex items-baseline gap-2">
|
||||
<dt className="text-muted-foreground text-xs uppercase tracking-wide">
|
||||
<dt className="text-muted-foreground text-xs tracking-wide uppercase">
|
||||
{t("settings.about.builtAt", { time: "" }).trim() ||
|
||||
t("settings.about.gitSha")}
|
||||
</dt>
|
||||
|
||||
@@ -487,7 +487,9 @@ function UserAIProviderSubsection() {
|
||||
queryClient.invalidateQueries({ queryKey: ["insights"] });
|
||||
},
|
||||
onError: (e) => {
|
||||
setSaveMsg(e instanceof Error ? e.message : t("settings.ai.errorGeneric"));
|
||||
setSaveMsg(
|
||||
e instanceof Error ? e.message : t("settings.ai.errorGeneric"),
|
||||
);
|
||||
setSaveOk(false);
|
||||
},
|
||||
});
|
||||
@@ -522,9 +524,7 @@ function UserAIProviderSubsection() {
|
||||
);
|
||||
setTestOk(true);
|
||||
} catch (e) {
|
||||
setTestMsg(
|
||||
e instanceof Error ? e.message : t("settings.ai.testFailed"),
|
||||
);
|
||||
setTestMsg(e instanceof Error ? e.message : t("settings.ai.testFailed"));
|
||||
setTestOk(false);
|
||||
} finally {
|
||||
setTesting(false);
|
||||
@@ -556,11 +556,15 @@ function UserAIProviderSubsection() {
|
||||
className="bg-background border-input mt-1 h-9 w-full rounded-md border px-2 text-sm"
|
||||
>
|
||||
<option value="">{t("settings.ai.providerOptionDefault")}</option>
|
||||
<option value="OPENAI">{t("settings.ai.providerOptionOpenai")}</option>
|
||||
<option value="OPENAI">
|
||||
{t("settings.ai.providerOptionOpenai")}
|
||||
</option>
|
||||
<option value="ANTHROPIC">
|
||||
{t("settings.ai.providerOptionAnthropic")}
|
||||
</option>
|
||||
<option value="LOCAL">{t("settings.ai.providerOptionLocal")}</option>
|
||||
<option value="LOCAL">
|
||||
{t("settings.ai.providerOptionLocal")}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,7 +3,12 @@
|
||||
import { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { SlidersHorizontal, RotateCcw, Loader2, AlertTriangle } from "lucide-react";
|
||||
import {
|
||||
SlidersHorizontal,
|
||||
RotateCcw,
|
||||
Loader2,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -91,7 +96,9 @@ export function ThresholdsSection({ id }: { id: string }) {
|
||||
toast.success(t("thresholds.saveSuccess"));
|
||||
},
|
||||
onError: (err) =>
|
||||
toast.error(err instanceof Error ? err.message : t("thresholds.saveError")),
|
||||
toast.error(
|
||||
err instanceof Error ? err.message : t("thresholds.saveError"),
|
||||
),
|
||||
});
|
||||
|
||||
const resetMutation = useMutation({
|
||||
@@ -114,7 +121,7 @@ export function ThresholdsSection({ id }: { id: string }) {
|
||||
return (
|
||||
<div
|
||||
id={id}
|
||||
className="bg-card border-border scroll-mt-28 rounded-xl border p-6 space-y-5"
|
||||
className="bg-card border-border scroll-mt-28 space-y-5 rounded-xl border p-6"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -138,7 +145,7 @@ export function ThresholdsSection({ id }: { id: string }) {
|
||||
</p>
|
||||
|
||||
{isLoading || !data ? (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<div className="text-muted-foreground flex items-center gap-2 text-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t("common.loading")}
|
||||
</div>
|
||||
@@ -202,7 +209,7 @@ function MetricRow({
|
||||
const defaultRange = effective?.default;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-border p-4 space-y-3">
|
||||
<div className="border-border space-y-3 rounded-lg border p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm font-medium">{t(METRIC_LABEL_KEYS[metric])}</p>
|
||||
@@ -212,7 +219,7 @@ function MetricRow({
|
||||
: t("thresholds.unsetExplanation")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Label htmlFor={`override-${metric}`} className="text-xs">
|
||||
{overrideMode
|
||||
? t("thresholds.overrideModeLabel")
|
||||
@@ -300,8 +307,8 @@ function MetricRow({
|
||||
defaultRange &&
|
||||
(override!.min < defaultRange.greenMin * 0.7 ||
|
||||
override!.max > defaultRange.greenMax * 1.3) && (
|
||||
<div className="flex items-start gap-2 rounded-md border-l-2 border-dracula-orange/50 bg-dracula-orange/5 p-2 text-xs">
|
||||
<AlertTriangle className="mt-0.5 h-3 w-3 shrink-0 text-dracula-orange" />
|
||||
<div className="border-dracula-orange/50 bg-dracula-orange/5 flex items-start gap-2 rounded-md border-l-2 p-2 text-xs">
|
||||
<AlertTriangle className="text-dracula-orange mt-0.5 h-3 w-3 shrink-0" />
|
||||
<span>{t("thresholds.overrideWarning")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -85,10 +85,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn(
|
||||
"flex flex-col gap-2 pr-9 text-left",
|
||||
className,
|
||||
)}
|
||||
className={cn("flex flex-col gap-2 pr-9 text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -23,10 +23,7 @@ export function PasswordStrength({
|
||||
minLength = 12,
|
||||
}: PasswordStrengthProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { translate } = useMemo(
|
||||
() => getZxcvbnTranslations(locale),
|
||||
[locale],
|
||||
);
|
||||
const { translate } = useMemo(() => getZxcvbnTranslations(locale), [locale]);
|
||||
const [result, setResult] = useState<{
|
||||
score: number;
|
||||
feedback: { warning: string; suggestions: string[] };
|
||||
|
||||
Reference in new issue
Block a user