mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
refactor(v1.4.19): apply simplify-review safe suggestions
Mechanical clean-up sweep across five hot spots flagged by the simplify
audit:
- Lift the 11-key auth.* audit-action label map out of the two admin
components that rebuilt it on every render. The shared
useAuthActionLabels() hook in _shared.tsx is now the single home for
new entries (login-overview + recent-audit-preview consume it).
- Drop the dead chipClass branches in IntegrationStatusPill: only the
connected state actually paints a Dracula-green chip; error and
disconnected fall back to the badge variant.
- Add thin DateInput / DateTimeInput wrappers under components/ui that
bake in the lang={locale} hint so 14 callsites across 9 files no
longer have to remember the prop. A future date input cannot
silently regress.
- Remove the leading icon-only flex wrappers left behind when the
duplicate card titles were dropped on /admin/feedback,
/admin/danger-zone, and the thresholds editor.
- Strip the five "v1.4.19 X removed because…" narration comments from
insights/page.tsx and the dashboard. git blame already answers what
changed and the comments aged into history.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ef74241a68
commit
6b35cad079
19 files changed
+108
-155
No files matched your search
@@ -221,13 +221,6 @@ interface MedicationComplianceDailyResponse {
|
||||
dailyCompliance: Record<string, MedicationDailyData>;
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.4.19 A3 — `getRangeColorClass` / `getRangeHint` /
|
||||
* `RangeDisplayConfig` removed alongside the `<TrendCard>` tile-strip
|
||||
* deletion. Those helpers were only used by the duplicate-of-dashboard
|
||||
* tile grid; every other call site lives on the dashboard surface.
|
||||
*/
|
||||
|
||||
/**
|
||||
* v1.4.16 phase B1b — pick the freshest ISO timestamp from a list so the
|
||||
* page hero's "Generated …" caption surfaces a single representative
|
||||
@@ -645,10 +638,6 @@ export default function InsightsPage() {
|
||||
const sys = analytics?.summaries?.BLOOD_PRESSURE_SYS;
|
||||
const dia = analytics?.summaries?.BLOOD_PRESSURE_DIA;
|
||||
const p = analytics?.summaries?.PULSE;
|
||||
// v1.4.19 A3 — `bf` / `showBodyFatCard` / `moodSummary` /
|
||||
// `showMoodCard` were bookkeeping for the now-removed duplicate
|
||||
// tile strip (Marc said those tiles duplicate the dashboard).
|
||||
// Remaining mood references read `data?.moodSummary` inline below.
|
||||
const bmiDivisor = user?.heightCm ? (user.heightCm / 100) ** 2 : null;
|
||||
// bmiAvg30 and bmiSlope30 still used for overallStatus + bmiSectionStatus
|
||||
const bmiAvg30 = bmiDivisor && w?.avg30 != null ? w.avg30 / bmiDivisor : null;
|
||||
@@ -853,12 +842,6 @@ export default function InsightsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* v1.4.19 A3 — comparison toggle is now mounted INSIDE the hero
|
||||
meta band (alongside personal-baseline indicator + Generated
|
||||
timestamp + Regenerate button). The previous standalone band
|
||||
below the hero made the page read as three stacked cards
|
||||
with separate header / control rows; folding the toggle into
|
||||
the hero clusters every page-level control in one place. */}
|
||||
<InsightsPageHero
|
||||
updatedAt={advisor.payload?.cachedAt ?? heroUpdatedAt}
|
||||
onRegenerate={advisor.regenerate}
|
||||
@@ -869,24 +852,8 @@ export default function InsightsPage() {
|
||||
{/* v1.4.16 phase D reconcile (CRITICAL C1) — wire the polished
|
||||
`<InsightAdvisorCard>` (severity-ordered recommendations grid +
|
||||
per-rec rationale expand + confidence meter + thumbs feedback +
|
||||
medical-citation footnotes) into the live route. Before this
|
||||
fix the page rendered only the text-only `<InsightStatusCard>`
|
||||
per section, hiding all of B5c/d/e/B1b's polish from users.
|
||||
The per-section status cards stay below as supplemental detail.
|
||||
|
||||
v1.4.19 A3 — `onRegenerate` is no longer wired here. The page
|
||||
hero already owns the single page-level refresh affordance;
|
||||
the card-level icon button + empty-state "Analyse starten"
|
||||
button were duplicates that surfaced the same handler three
|
||||
ways (page hero button, card-header icon button,
|
||||
empty-state CTA). The per-recommendation Regenerate (inside
|
||||
`<RecommendationCard>`) is unrelated — it stays under the
|
||||
v1.4.16 spec. The placeholder `aiOverviewTitle`
|
||||
("Persönlicher Berater") subtitle is also gone — the
|
||||
CardTitle ("KI-Gesundheitsanalyse") is enough framing on its
|
||||
own, and the orphan title above an empty body when the user
|
||||
has no provider configured was the actual confusion source
|
||||
Marc reported. */}
|
||||
medical-citation footnotes) into the live route. The
|
||||
per-section status cards stay below as supplemental detail. */}
|
||||
<InsightAdvisorCard
|
||||
insight={advisor.payload?.insights ?? null}
|
||||
loading={advisor.isLoading}
|
||||
@@ -897,15 +864,6 @@ export default function InsightsPage() {
|
||||
|
||||
<InsightsSectionNav />
|
||||
|
||||
{/* v1.4.19 A3 — duplicate per-metric `<TrendCard>` strip removed.
|
||||
Up to v1.4.18 the page led with a 5-column grid of weight /
|
||||
BP-sys / BP-dia / pulse / body-fat / mood tiles — exact
|
||||
duplicates of the dashboard tile-strip. They added zero
|
||||
information beyond what's already on the dashboard. The
|
||||
recommendations grid + per-section detail cards below are
|
||||
the actual value on `/insights`; the tiles only consumed
|
||||
scroll height. */}
|
||||
|
||||
<section id="section-general" className="scroll-mt-28 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-semibold">
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Logo } from "@/components/ui/logo";
|
||||
import { MeasurementForm } from "@/components/measurements/measurement-form";
|
||||
@@ -301,10 +302,8 @@ export default function OnboardingPage() {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ob-dob">{t("settings.dateOfBirth")}</Label>
|
||||
<Input
|
||||
<DateInput
|
||||
id="ob-dob"
|
||||
type="date"
|
||||
lang={locale}
|
||||
value={dateOfBirth}
|
||||
onChange={(e) => setDateOfBirth(e.target.value)}
|
||||
max={new Date().toISOString().slice(0, 10)}
|
||||
|
||||
@@ -491,17 +491,6 @@ export default function DashboardPage() {
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
{/* v1.4.19 A3 — comparison toggle removed from dashboard.
|
||||
The tile strip is too tight for an extra control band, and
|
||||
Marc rarely wants Vormonat / Vorjahr context at a glance —
|
||||
when he does, it's on `/insights` where the chart sits next
|
||||
to the toggle. The underlying `compareBaseline` value still
|
||||
flows from `/api/dashboard/widgets` into every chart below
|
||||
so nothing visual changes when the user has the comparison
|
||||
on; only the on-surface affordance moves. Settings →
|
||||
Dashboard remains the power-user backdoor for either
|
||||
surface. */}
|
||||
|
||||
{/* v1.4: Getting-started checklist for brand-new users.
|
||||
* Self-gates visibility on (onboardingCompletedAt == null
|
||||
* || measurementCount < 5) and disappears once dismissed
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
@@ -257,6 +258,34 @@ export async function getApiErrorMessage(response: Response): Promise<string> {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared `auth.*` audit-action label map. Two surfaces consume it
|
||||
* today (`/admin/login-overview` and the dashboard recent-activity
|
||||
* preview), and any future audit feed will need the same translation
|
||||
* lookup. New auth actions are added here once instead of being
|
||||
* copy-pasted into every consumer.
|
||||
*/
|
||||
export function useAuthActionLabels(): Record<string, string> {
|
||||
const { t, locale } = useTranslations();
|
||||
return useMemo(
|
||||
() => ({
|
||||
"auth.register": t("admin.authRegister"),
|
||||
"auth.login": t("admin.authLogin"),
|
||||
"auth.login.passkey": t("admin.authLoginPasskey"),
|
||||
"auth.login.password": t("admin.authLoginPassword"),
|
||||
"auth.login.failed": t("admin.authLoginFailed"),
|
||||
"auth.logout": t("admin.authLogout"),
|
||||
"auth.passkey.register": t("admin.authPasskeyRegister"),
|
||||
"auth.passkey.delete": t("admin.authPasskeyDelete"),
|
||||
"auth.token.autoissue.native": t("admin.authTokenAutoissueNative"),
|
||||
"auth.token.refresh": t("admin.authTokenRefresh"),
|
||||
"auth.token.revoke": t("admin.authTokenRevoke"),
|
||||
}),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[locale],
|
||||
);
|
||||
}
|
||||
|
||||
export function useSystemStatus() {
|
||||
return useQuery({
|
||||
queryKey: ["admin", "status"],
|
||||
|
||||
@@ -70,12 +70,10 @@ export function DangerZoneSection() {
|
||||
already names the section, so the card-level title was a
|
||||
duplicate of the page title. The destructive icon stays so
|
||||
the card still flags itself as dangerous at a glance. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle
|
||||
className="text-destructive h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<AlertTriangle
|
||||
className="text-destructive h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm font-medium">{t("admin.deleteAllData")}</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
|
||||
@@ -78,9 +78,7 @@ export function FeedbackInboxSection() {
|
||||
`/admin/feedback`, so the page header already provides the
|
||||
title + description — the card-level repetition was just
|
||||
visual noise. The icon stays as a low-key card affordance. */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Inbox className="text-primary h-5 w-5" aria-hidden="true" />
|
||||
</div>
|
||||
<Inbox className="text-primary h-5 w-5" aria-hidden="true" />
|
||||
|
||||
<Tabs
|
||||
value={activeStatus}
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { toCSV } from "@/lib/export";
|
||||
import { type AdminAuditEntry } from "./_shared";
|
||||
import { type AdminAuditEntry, useAuthActionLabels } from "./_shared";
|
||||
|
||||
type DateRangePreset = "all" | "24h" | "7d" | "30d";
|
||||
type PerPageValue = 25 | 50 | 100;
|
||||
@@ -67,19 +67,7 @@ export function LoginOverviewSection() {
|
||||
const [page, setPage] = useState(1);
|
||||
const [perPage, setPerPage] = useState<PerPageValue>(50);
|
||||
|
||||
const AUTH_ACTION_LABELS: Record<string, string> = {
|
||||
"auth.register": t("admin.authRegister"),
|
||||
"auth.login": t("admin.authLogin"),
|
||||
"auth.login.passkey": t("admin.authLoginPasskey"),
|
||||
"auth.login.password": t("admin.authLoginPassword"),
|
||||
"auth.login.failed": t("admin.authLoginFailed"),
|
||||
"auth.logout": t("admin.authLogout"),
|
||||
"auth.passkey.register": t("admin.authPasskeyRegister"),
|
||||
"auth.passkey.delete": t("admin.authPasskeyDelete"),
|
||||
"auth.token.autoissue.native": t("admin.authTokenAutoissueNative"),
|
||||
"auth.token.refresh": t("admin.authTokenRefresh"),
|
||||
"auth.token.revoke": t("admin.authTokenRevoke"),
|
||||
};
|
||||
const AUTH_ACTION_LABELS = useAuthActionLabels();
|
||||
|
||||
// Build the query string once so it's reused by the data-query key, the
|
||||
// export download, and the next-/prev- buttons.
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { type AdminAuditEntry } from "./_shared";
|
||||
import { type AdminAuditEntry, useAuthActionLabels } from "./_shared";
|
||||
|
||||
const PREVIEW_LIMIT = 10;
|
||||
|
||||
@@ -47,19 +47,7 @@ export function RecentAuditPreview() {
|
||||
staleTime: 30_000,
|
||||
});
|
||||
|
||||
const AUTH_ACTION_LABELS: Record<string, string> = {
|
||||
"auth.register": t("admin.authRegister"),
|
||||
"auth.login": t("admin.authLogin"),
|
||||
"auth.login.passkey": t("admin.authLoginPasskey"),
|
||||
"auth.login.password": t("admin.authLoginPassword"),
|
||||
"auth.login.failed": t("admin.authLoginFailed"),
|
||||
"auth.logout": t("admin.authLogout"),
|
||||
"auth.passkey.register": t("admin.authPasskeyRegister"),
|
||||
"auth.passkey.delete": t("admin.authPasskeyDelete"),
|
||||
"auth.token.autoissue.native": t("admin.authTokenAutoissueNative"),
|
||||
"auth.token.refresh": t("admin.authTokenRefresh"),
|
||||
"auth.token.revoke": t("admin.authTokenRevoke"),
|
||||
};
|
||||
const AUTH_ACTION_LABELS = useAuthActionLabels();
|
||||
|
||||
const entries = data?.entries ?? [];
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
@@ -80,7 +81,7 @@ export function DoctorReportDialog({
|
||||
defaultPracticeName,
|
||||
onSubmit,
|
||||
}: DoctorReportDialogProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const [startDate, setStartDate] = useState<string>(defaultStartIso);
|
||||
const [endDate, setEndDate] = useState<string>(todayIso);
|
||||
const [practiceName, setPracticeName] = useState<string>("");
|
||||
@@ -191,10 +192,8 @@ export function DoctorReportDialog({
|
||||
<Label htmlFor="dr-start">
|
||||
{t("doctorReport.dialog.startLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateInput
|
||||
id="dr-start"
|
||||
type="date"
|
||||
lang={locale}
|
||||
value={startDate}
|
||||
max={endDate || todayIso()}
|
||||
onChange={(e) => {
|
||||
@@ -208,10 +207,8 @@ export function DoctorReportDialog({
|
||||
<Label htmlFor="dr-end">
|
||||
{t("doctorReport.dialog.endLabel")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateInput
|
||||
id="dr-end"
|
||||
type="date"
|
||||
lang={locale}
|
||||
value={endDate}
|
||||
min={startDate}
|
||||
max={todayIso()}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateTimeInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
@@ -117,7 +118,7 @@ export function MeasurementForm({
|
||||
onCancel,
|
||||
defaultType,
|
||||
}: MeasurementFormProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Normalize legacy BP types to combined mode
|
||||
@@ -358,10 +359,8 @@ export function MeasurementForm({
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="measuredAt">{t("measurements.timestamp")}</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="measuredAt"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={measuredAt}
|
||||
onChange={(e) => setMeasuredAt(e.target.value)}
|
||||
required
|
||||
|
||||
@@ -62,6 +62,7 @@ import { useState } from "react";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys";
|
||||
import { DateTimeInput } from "@/components/ui/date-input";
|
||||
import {
|
||||
MEASUREMENT_TYPE_LABEL_KEYS as TYPE_LABEL_KEYS,
|
||||
MEASUREMENT_TYPE_ICONS as TYPE_ICONS,
|
||||
@@ -120,7 +121,7 @@ function formatMeasurementSource(
|
||||
}
|
||||
|
||||
export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -583,10 +584,8 @@ export function MeasurementList({ onEdit, onAddFirst }: MeasurementListProps) {
|
||||
<Label htmlFor="edit-measuredAt">
|
||||
{t("measurements.timestamp")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="edit-measuredAt"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={editMeasuredAt}
|
||||
onChange={(e) => setEditMeasuredAt(e.target.value)}
|
||||
required
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateTimeInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
@@ -95,7 +95,7 @@ export function IntakeHistoryList({
|
||||
}: IntakeHistoryListProps) {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
|
||||
const sourceLabels: Record<string, string> = {
|
||||
WEB: t("medications.sourceWeb"),
|
||||
@@ -542,10 +542,8 @@ export function IntakeHistoryList({
|
||||
<Label htmlFor="edit-scheduledFor">
|
||||
{t("medications.intakeScheduledFor")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="edit-scheduledFor"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={editScheduledFor}
|
||||
onChange={(e) => setEditScheduledFor(e.target.value)}
|
||||
required
|
||||
@@ -556,10 +554,8 @@ export function IntakeHistoryList({
|
||||
<Label htmlFor="edit-takenAt">
|
||||
{t("medications.intakeTakenAt")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="edit-takenAt"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={editTakenAt}
|
||||
onChange={(e) => setEditTakenAt(e.target.value)}
|
||||
disabled={editSkipped}
|
||||
@@ -688,10 +684,8 @@ export function IntakeHistoryList({
|
||||
<Label htmlFor="create-scheduledFor">
|
||||
{t("medications.intakeScheduledFor")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="create-scheduledFor"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={createScheduledFor}
|
||||
onChange={(e) => setCreateScheduledFor(e.target.value)}
|
||||
required
|
||||
@@ -702,10 +696,8 @@ export function IntakeHistoryList({
|
||||
<Label htmlFor="create-takenAt">
|
||||
{t("medications.intakeTakenAt")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="create-takenAt"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={createTakenAt}
|
||||
onChange={(e) => setCreateTakenAt(e.target.value)}
|
||||
disabled={createSkipped}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateTimeInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Loader2, MoreHorizontal, Plus, RotateCcw } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@@ -37,7 +38,7 @@ function getDefaultMoodLoggedAtValue() {
|
||||
}
|
||||
|
||||
export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [mood, setMood] = useState("");
|
||||
@@ -132,10 +133,8 @@ export function MoodForm({ onSuccess, onCancel }: MoodFormProps) {
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mood-logged-at">{t("mood.timestamp")}</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="mood-logged-at"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={moodLoggedAt}
|
||||
onChange={(e) => setMoodLoggedAt(e.target.value)}
|
||||
required
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateTimeInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Table,
|
||||
@@ -117,7 +118,7 @@ interface MoodListProps {
|
||||
}
|
||||
|
||||
export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -569,10 +570,8 @@ export function MoodList({ onAddFirst }: MoodListProps = {}) {
|
||||
<Label htmlFor="edit-mood-logged-at">
|
||||
{t("mood.timestamp")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateTimeInput
|
||||
id="edit-mood-logged-at"
|
||||
type="datetime-local"
|
||||
lang={locale}
|
||||
value={editMoodLoggedAt}
|
||||
onChange={(e) => setEditMoodLoggedAt(e.target.value)}
|
||||
required
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/settings/password-input";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -383,10 +384,8 @@ export function AccountSection() {
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dob">{t("settings.dateOfBirth")}</Label>
|
||||
<Input
|
||||
<DateInput
|
||||
id="dob"
|
||||
type="date"
|
||||
lang={locale}
|
||||
value={dateOfBirth}
|
||||
onChange={(e) => setDateOfBirth(e.target.value)}
|
||||
max={new Date().toISOString().slice(0, 10)}
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { DateInput } from "@/components/ui/date-input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
@@ -253,17 +253,15 @@ function DateRangeFields({
|
||||
setUntil,
|
||||
idPrefix,
|
||||
}: DateRangeFieldsProps) {
|
||||
const { t, locale } = useTranslations();
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor={`${idPrefix}-since`} className="text-xs">
|
||||
{t("settings.sections.export.filters.since")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateInput
|
||||
id={`${idPrefix}-since`}
|
||||
type="date"
|
||||
lang={locale}
|
||||
value={since}
|
||||
onChange={(e) => setSince(e.target.value)}
|
||||
max={until || undefined}
|
||||
@@ -273,10 +271,8 @@ function DateRangeFields({
|
||||
<Label htmlFor={`${idPrefix}-until`} className="text-xs">
|
||||
{t("settings.sections.export.filters.until")}
|
||||
</Label>
|
||||
<Input
|
||||
<DateInput
|
||||
id={`${idPrefix}-until`}
|
||||
type="date"
|
||||
lang={locale}
|
||||
value={until}
|
||||
onChange={(e) => setUntil(e.target.value)}
|
||||
min={since || undefined}
|
||||
|
||||
@@ -74,26 +74,26 @@ export function IntegrationStatusPill({
|
||||
}: IntegrationStatusPillProps) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
// chipClass only applies to the `connected` branch — `error` falls back
|
||||
// to `variant="destructive"` and `disconnected` to `variant="outline"`.
|
||||
const connectedChipClass =
|
||||
"border-dracula-green/30 bg-dracula-green/15 text-dracula-green";
|
||||
|
||||
let label: string;
|
||||
let icon: React.ReactNode;
|
||||
let chipClass: string;
|
||||
|
||||
switch (state) {
|
||||
case "connected":
|
||||
label = t("settings.integrationPill.connected");
|
||||
icon = <CheckCircle2 aria-hidden="true" className="h-3 w-3" />;
|
||||
chipClass =
|
||||
"border-dracula-green/30 bg-dracula-green/15 text-dracula-green";
|
||||
break;
|
||||
case "error":
|
||||
label = t("settings.integrationPill.errorReconnect");
|
||||
icon = <AlertTriangle aria-hidden="true" className="h-3 w-3" />;
|
||||
chipClass = "";
|
||||
break;
|
||||
case "disconnected":
|
||||
label = t("settings.integrationPill.notConnected");
|
||||
icon = <CircleSlash aria-hidden="true" className="h-3 w-3" />;
|
||||
chipClass = "";
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export function IntegrationStatusPill({
|
||||
aria-label={t("settings.integrationPill.ariaLabel")}
|
||||
className={cn(
|
||||
"max-w-full whitespace-nowrap",
|
||||
state === "connected" && chipClass,
|
||||
state === "connected" && connectedChipClass,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -129,12 +129,10 @@ export function ThresholdsEditorSection({ id }: { id: string }) {
|
||||
Keep the icon + reset action so the card still has a visible
|
||||
control affordance. */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<SlidersHorizontal
|
||||
className="text-primary h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<SlidersHorizontal
|
||||
className="text-primary h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{data && Object.keys(data.overrides).length > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* Native date input that hints the active locale via `lang=`. Browsers
|
||||
* use this hint to render the calendar widget in dd.MM.yyyy / MM/dd/yyyy
|
||||
* order, which is the user-visible format we want to follow the app's
|
||||
* EN/DE preference. Wrapping the prop here keeps every callsite from
|
||||
* having to pull `locale` out of `useTranslations()` just to feed it
|
||||
* back into the same JSX prop, and guarantees a future date input
|
||||
* cannot silently regress by forgetting the hint.
|
||||
*/
|
||||
type InputProps = React.ComponentProps<typeof Input>;
|
||||
type DateInputProps = Omit<InputProps, "type" | "lang">;
|
||||
|
||||
export function DateInput(props: DateInputProps) {
|
||||
const { locale } = useTranslations();
|
||||
return <Input type="date" lang={locale} {...props} />;
|
||||
}
|
||||
|
||||
export function DateTimeInput(props: DateInputProps) {
|
||||
const { locale } = useTranslations();
|
||||
return <Input type="datetime-local" lang={locale} {...props} />;
|
||||
}
|
||||
Reference in new issue
Block a user