mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(insights): give the cycle ring a score cell only when it has a dial
The scores strip reserved a cell for the cycle ring, and dropped Strain to make room, whenever the cycle module was on. Without an active cycle the ring renders nothing, so the row showed a gap where Strain used to be. The overview now asks the same calendar read whether there is a dial today.
This commit is contained in:
1 parent
5f6a0e42b4
commit
a6c65bd37e
5 files changed
+103
-17
No files matched your search
@@ -48,7 +48,7 @@ vi.mock("../use-cycle", async () => {
|
||||
await vi.importActual<typeof import("../use-cycle")>("../use-cycle");
|
||||
return {
|
||||
...actual,
|
||||
useCycleCalendar: () => calendarState.current,
|
||||
useCycleRingCalendar: () => calendarState.current,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* The scores strip gives the cycle ring a cell only when the ring would draw
|
||||
* a dial. The ring renders nothing without an active cycle, and a cell
|
||||
* reserved for it (with Strain dropped to make room) sat empty in the row.
|
||||
*/
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
import { localYmd, useCycleRingHasDial } from "../use-cycle";
|
||||
|
||||
function window(): [string, string] {
|
||||
const from = new Date();
|
||||
from.setDate(from.getDate() - 90);
|
||||
const to = new Date();
|
||||
to.setDate(to.getDate() + 180);
|
||||
return [localYmd(from), localYmd(to)];
|
||||
}
|
||||
|
||||
function Probe({ enabled }: { enabled: boolean }) {
|
||||
return <span>{String(useCycleRingHasDial(enabled))}</span>;
|
||||
}
|
||||
|
||||
function hasDial(verdict: unknown, enabled = true): boolean {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { staleTime: Infinity, retry: 0 } },
|
||||
});
|
||||
const [from, to] = window();
|
||||
client.setQueryData(queryKeys.cycleCalendar(from, to), { verdict });
|
||||
const html = renderToStaticMarkup(
|
||||
<QueryClientProvider client={client}>
|
||||
<Probe enabled={enabled} />
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
return html === "<span>true</span>";
|
||||
}
|
||||
|
||||
describe("useCycleRingHasDial", () => {
|
||||
it("is true while a cycle is active today", () => {
|
||||
expect(hasDial({ phase: "follicular", dayOfCycle: 6 })).toBe(true);
|
||||
});
|
||||
|
||||
it("is false without an active cycle, so no empty cell is reserved", () => {
|
||||
expect(hasDial({ phase: null, dayOfCycle: null })).toBe(false);
|
||||
expect(hasDial(null)).toBe(false);
|
||||
});
|
||||
|
||||
it("is false while the cycle surface is hidden", () => {
|
||||
expect(hasDial({ phase: "luteal", dayOfCycle: 20 }, false)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Droplets } from "lucide-react";
|
||||
|
||||
@@ -8,7 +8,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { CycleRing } from "./cycle-ring";
|
||||
import { PHASE_HUE } from "./phase-tokens";
|
||||
import { localYmd, useCycleCalendar } from "./use-cycle";
|
||||
import { useCycleRingCalendar } from "./use-cycle";
|
||||
|
||||
/**
|
||||
* v1.15.3 — the compact cycle ring as a WELLNESS-STRIP element.
|
||||
@@ -36,22 +36,12 @@ import { localYmd, useCycleCalendar } from "./use-cycle";
|
||||
* strip (no gap, no placeholder).
|
||||
*/
|
||||
|
||||
/** YYYY-MM-DD for `n` days from now in the local tz (shares `localYmd`). */
|
||||
function shiftToday(days: number): string {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + days);
|
||||
return localYmd(d);
|
||||
}
|
||||
|
||||
export function CycleRingTile({ className }: { className?: string }) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
// Mirror the cycle-view + summary-card window so the cache key is shared —
|
||||
// no extra request when the user opens the cycle page in the same session.
|
||||
const from = useMemo(() => shiftToday(-90), []);
|
||||
const to = useMemo(() => shiftToday(180), []);
|
||||
|
||||
const calendar = useCycleCalendar(from, to);
|
||||
const calendar = useCycleRingCalendar();
|
||||
// The server already resolved the ring: cycle day, phase, and the arc
|
||||
// proportions (including the profile-derived idealized ring a low-data
|
||||
// tracker gets). The tile renders it.
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* wheel, predictions panel, and history repaint in lockstep after a quick
|
||||
* log.
|
||||
*/
|
||||
import { useMemo } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -111,6 +112,35 @@ export function useCycleCalendar(from: string, to: string, enabled = true) {
|
||||
});
|
||||
}
|
||||
|
||||
/** YYYY-MM-DD for `days` from today in the local zone. */
|
||||
function localYmdFromToday(days: number): string {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() + days);
|
||||
return localYmd(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* The calendar window the cycle ring reads: the cycle page's own window, so
|
||||
* the ring, the page and the summary card share one cache entry.
|
||||
*/
|
||||
export function useCycleRingCalendar(enabled = true) {
|
||||
const from = useMemo(() => localYmdFromToday(-90), []);
|
||||
const to = useMemo(() => localYmdFromToday(180), []);
|
||||
return useCycleCalendar(from, to, enabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the cycle ring would draw a dial today. The wellness strip asks
|
||||
* this before it gives the ring a cell (and takes Strain's place for it): the
|
||||
* ring renders nothing without an active cycle, and a cell reserved for it
|
||||
* would sit empty in the row.
|
||||
*/
|
||||
export function useCycleRingHasDial(enabled: boolean): boolean {
|
||||
const calendar = useCycleRingCalendar(enabled);
|
||||
const verdict = calendar.data?.verdict;
|
||||
return enabled && verdict?.phase != null && verdict.dayOfCycle != null;
|
||||
}
|
||||
|
||||
export function useCycleHistory(limit = 24) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.cycleHistory(limit),
|
||||
|
||||
Reference in new issue
Block a user