mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
refactor: simplify-pass apply-yes cleanup
Five independent atomic edits from the W5 simplify review, grouped in a single commit since none pull weight as a compound theme: - coach-input: collapse `!disabled && value.trim().length > 0` triplication onto a single `canSubmit` (S-01). - hero-strip: extract the duplicate weekly-report Button shell into a shared label fragment with the Link wrapping conditionally (S-02). - api-token-overview-section: extract `<TokenStatusBadge size>` so the desktop table and mobile card list stay in lock-step on the revoked / expired / active 3-way ternary (S-03). - trends-row: hoist the dynamic-load chart skeleton to a shared `ChartSkeleton` component (S-04). No chart-visual change — placeholder only. - correlation-row: collapse the grid className ternary onto `cn()` with a conditional `md:grid-cols-2` (S-05). Test count unchanged (2105). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
65a8b5a8b6
commit
1631d8042c
5 files changed
+98
-92
No files matched your search
@@ -55,6 +55,46 @@ function TruncatedCell({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* v1.4.22 W5 reconcile (S-03) — the revoked / isExpired / active
|
||||||
|
* 3-way ternary is identical between the desktop table and the
|
||||||
|
* mobile card list; the only difference is `text-xs` vs `text-[10px]`.
|
||||||
|
* Centralise so the two surfaces stay in lock-step.
|
||||||
|
*/
|
||||||
|
function TokenStatusBadge({
|
||||||
|
token,
|
||||||
|
size,
|
||||||
|
}: {
|
||||||
|
token: ApiTokenInfo;
|
||||||
|
size: "sm" | "xs";
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslations();
|
||||||
|
const expired =
|
||||||
|
token.expiresAt != null && new Date(token.expiresAt) < new Date();
|
||||||
|
const sizeClass = size === "xs" ? "text-[10px]" : "text-xs";
|
||||||
|
if (token.revoked) {
|
||||||
|
return (
|
||||||
|
<Badge variant="destructive" className={`shrink-0 ${sizeClass}`}>
|
||||||
|
{t("settings.tokenRevoked")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (expired) {
|
||||||
|
return (
|
||||||
|
<Badge variant="destructive" className={`shrink-0 ${sizeClass}`}>
|
||||||
|
{t("settings.tokenExpired")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Badge
|
||||||
|
className={`bg-dracula-green/15 text-dracula-green shrink-0 ${sizeClass}`}
|
||||||
|
>
|
||||||
|
{t("common.active")}
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* F-18 (v1.4.19): the auto-login flow names tokens
|
* F-18 (v1.4.19): the auto-login flow names tokens
|
||||||
* "web auto-login 2026-05-05T19:46:20.603Z" / "iOS auto-login …" by
|
* "web auto-login 2026-05-05T19:46:20.603Z" / "iOS auto-login …" by
|
||||||
@@ -181,8 +221,6 @@ export function ApiTokenOverviewSection() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-border divide-y">
|
<tbody className="divide-border divide-y">
|
||||||
{tokens.map((token, i) => {
|
{tokens.map((token, i) => {
|
||||||
const isExpired =
|
|
||||||
token.expiresAt && new Date(token.expiresAt) < new Date();
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={token.id}
|
key={token.id}
|
||||||
@@ -213,19 +251,7 @@ export function ApiTokenOverviewSection() {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 text-right">
|
<td className="px-3 py-2 text-right">
|
||||||
{token.revoked ? (
|
<TokenStatusBadge token={token} size="sm" />
|
||||||
<Badge variant="destructive" className="text-xs">
|
|
||||||
{t("settings.tokenRevoked")}
|
|
||||||
</Badge>
|
|
||||||
) : isExpired ? (
|
|
||||||
<Badge variant="destructive" className="text-xs">
|
|
||||||
{t("settings.tokenExpired")}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge className="bg-dracula-green/15 text-dracula-green text-xs">
|
|
||||||
{t("common.active")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</td>
|
</td>
|
||||||
{/* v1.4.22 C2 (5th attempt) — drop
|
{/* v1.4.22 C2 (5th attempt) — drop
|
||||||
`whitespace-nowrap` on the date cells. The
|
`whitespace-nowrap` on the date cells. The
|
||||||
@@ -264,8 +290,6 @@ export function ApiTokenOverviewSection() {
|
|||||||
data-testid="admin-tokens-mobile-list"
|
data-testid="admin-tokens-mobile-list"
|
||||||
>
|
>
|
||||||
{tokens.map((token) => {
|
{tokens.map((token) => {
|
||||||
const isExpired =
|
|
||||||
token.expiresAt && new Date(token.expiresAt) < new Date();
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={token.id}
|
key={token.id}
|
||||||
@@ -278,25 +302,7 @@ export function ApiTokenOverviewSection() {
|
|||||||
value={formatTokenName(token.name)}
|
value={formatTokenName(token.name)}
|
||||||
className="min-w-0 flex-1 font-medium"
|
className="min-w-0 flex-1 font-medium"
|
||||||
/>
|
/>
|
||||||
{token.revoked ? (
|
<TokenStatusBadge token={token} size="xs" />
|
||||||
<Badge
|
|
||||||
variant="destructive"
|
|
||||||
className="shrink-0 text-[10px]"
|
|
||||||
>
|
|
||||||
{t("settings.tokenRevoked")}
|
|
||||||
</Badge>
|
|
||||||
) : isExpired ? (
|
|
||||||
<Badge
|
|
||||||
variant="destructive"
|
|
||||||
className="shrink-0 text-[10px]"
|
|
||||||
>
|
|
||||||
{t("settings.tokenExpired")}
|
|
||||||
</Badge>
|
|
||||||
) : (
|
|
||||||
<Badge className="bg-dracula-green/15 text-dracula-green shrink-0 text-[10px]">
|
|
||||||
{t("common.active")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<TruncatedCell
|
<TruncatedCell
|
||||||
value={token.user.username}
|
value={token.user.username}
|
||||||
|
|||||||
@@ -56,32 +56,26 @@ export function CoachInput({
|
|||||||
}: CoachInputProps) {
|
}: CoachInputProps) {
|
||||||
const { t } = useTranslations();
|
const { t } = useTranslations();
|
||||||
|
|
||||||
|
const canSubmit = !disabled && value.trim().length > 0;
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
(event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (event.key !== "Enter") return;
|
// Shift+Enter inserts a newline; plain Enter and ⌘/Ctrl+Enter submit.
|
||||||
// Shift+Enter inserts a newline.
|
if (event.key !== "Enter" || event.shiftKey) return;
|
||||||
if (event.shiftKey) return;
|
|
||||||
// Plain Enter and ⌘/Ctrl+Enter both submit.
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!disabled && value.trim().length > 0) {
|
if (canSubmit) onSubmit();
|
||||||
onSubmit();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[disabled, onSubmit, value],
|
[canSubmit, onSubmit],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFormSubmit = useCallback(
|
const handleFormSubmit = useCallback(
|
||||||
(event: FormEvent<HTMLFormElement>) => {
|
(event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!disabled && value.trim().length > 0) {
|
if (canSubmit) onSubmit();
|
||||||
onSubmit();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[disabled, onSubmit, value],
|
[canSubmit, onSubmit],
|
||||||
);
|
);
|
||||||
|
|
||||||
const canSubmit = !disabled && value.trim().length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
data-slot="coach-input"
|
data-slot="coach-input"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslations } from "@/lib/i18n/context";
|
import { useTranslations } from "@/lib/i18n/context";
|
||||||
import type { CorrelationResult } from "@/lib/insights/correlations";
|
import type { CorrelationResult } from "@/lib/insights/correlations";
|
||||||
import { CorrelationCard } from "./correlation-card";
|
import { CorrelationCard } from "./correlation-card";
|
||||||
@@ -57,11 +58,10 @@ export function CorrelationRow({ results }: CorrelationRowProps) {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={
|
className={cn(
|
||||||
okResults.length === 1
|
"grid grid-cols-1 gap-4",
|
||||||
? "grid grid-cols-1 gap-4"
|
okResults.length > 1 && "md:grid-cols-2",
|
||||||
: "grid grid-cols-1 gap-4 md:grid-cols-2"
|
)}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{okResults.map((result) => (
|
{okResults.map((result) => (
|
||||||
<CorrelationCard key={result.kind} result={result} />
|
<CorrelationCard key={result.kind} result={result} />
|
||||||
|
|||||||
@@ -238,33 +238,41 @@ export function HeroStrip({
|
|||||||
* current ISO week. Older parents that haven't adopted the
|
* current ISO week. Older parents that haven't adopted the
|
||||||
* weeklyReportHref prop still get the disabled affordance.
|
* weeklyReportHref prop still get the disabled affordance.
|
||||||
*/}
|
*/}
|
||||||
{weeklyReportHref ? (
|
{(() => {
|
||||||
<Button
|
// v1.4.22 W5 reconcile (S-02) — collapse the
|
||||||
asChild
|
// enabled/disabled twin Buttons (variant, size, slot,
|
||||||
variant="default"
|
// class, icon, label all identical) into a single
|
||||||
size="sm"
|
// shared structure with the Link wrapping conditionally.
|
||||||
data-slot="insights-hero-strip-action-weekly-report"
|
const weeklyLabel = (
|
||||||
className="gap-1.5"
|
<>
|
||||||
>
|
|
||||||
<Link href={weeklyReportHref}>
|
|
||||||
<FileText className="h-3.5 w-3.5" aria-hidden="true" />
|
<FileText className="h-3.5 w-3.5" aria-hidden="true" />
|
||||||
<span>{t("insights.heroActionWeeklyReport")}</span>
|
<span>{t("insights.heroActionWeeklyReport")}</span>
|
||||||
</Link>
|
</>
|
||||||
</Button>
|
);
|
||||||
) : (
|
return weeklyReportHref ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
asChild
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled
|
data-slot="insights-hero-strip-action-weekly-report"
|
||||||
title={comingSoon}
|
className="gap-1.5"
|
||||||
data-slot="insights-hero-strip-action-weekly-report"
|
>
|
||||||
className="gap-1.5"
|
<Link href={weeklyReportHref}>{weeklyLabel}</Link>
|
||||||
>
|
</Button>
|
||||||
<FileText className="h-3.5 w-3.5" aria-hidden="true" />
|
) : (
|
||||||
<span>{t("insights.heroActionWeeklyReport")}</span>
|
<Button
|
||||||
</Button>
|
type="button"
|
||||||
)}
|
variant="default"
|
||||||
|
size="sm"
|
||||||
|
disabled
|
||||||
|
title={comingSoon}
|
||||||
|
data-slot="insights-hero-strip-action-weekly-report"
|
||||||
|
className="gap-1.5"
|
||||||
|
>
|
||||||
|
{weeklyLabel}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
{/* B2b wires this into the Coach drawer. The button is
|
{/* B2b wires this into the Coach drawer. The button is
|
||||||
enabled whenever the parent supplies an `onAskCoach`
|
enabled whenever the parent supplies an `onAskCoach`
|
||||||
handler; older parents that haven't adopted B2b yet
|
handler; older parents that haven't adopted B2b yet
|
||||||
|
|||||||
@@ -26,17 +26,20 @@ import {
|
|||||||
* the row's visual rhythm.
|
* the row's visual rhythm.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// v1.4.22 W5 reconcile (S-04) — both `dynamic()` calls used the same
|
||||||
|
// pulse-skeleton placeholder. Hoist so the loading affordance stays
|
||||||
|
// in lock-step (cleared against `feedback_charts_visual_identity.md`
|
||||||
|
// — placeholder only, no chart visual change).
|
||||||
|
const ChartSkeleton = () => (
|
||||||
|
<div className="bg-muted/40 h-[220px] w-full animate-pulse rounded-md motion-reduce:animate-none" />
|
||||||
|
);
|
||||||
|
|
||||||
const HealthChart = dynamic(
|
const HealthChart = dynamic(
|
||||||
() =>
|
() =>
|
||||||
import("@/components/charts/health-chart").then((mod) => ({
|
import("@/components/charts/health-chart").then((mod) => ({
|
||||||
default: mod.HealthChart,
|
default: mod.HealthChart,
|
||||||
})),
|
})),
|
||||||
{
|
{ ssr: false, loading: ChartSkeleton },
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<div className="bg-muted/40 h-[220px] w-full animate-pulse rounded-md motion-reduce:animate-none" />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const MoodChart = dynamic(
|
const MoodChart = dynamic(
|
||||||
@@ -44,12 +47,7 @@ const MoodChart = dynamic(
|
|||||||
import("@/components/charts/mood-chart").then((mod) => ({
|
import("@/components/charts/mood-chart").then((mod) => ({
|
||||||
default: mod.MoodChart,
|
default: mod.MoodChart,
|
||||||
})),
|
})),
|
||||||
{
|
{ ssr: false, loading: ChartSkeleton },
|
||||||
ssr: false,
|
|
||||||
loading: () => (
|
|
||||||
<div className="bg-muted/40 h-[220px] w-full animate-pulse rounded-md motion-reduce:animate-none" />
|
|
||||||
),
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
interface TrendsRowProps {
|
interface TrendsRowProps {
|
||||||
|
|||||||
Reference in new issue
Block a user