From 75a0f00be9b160aded265839e2f295f912552f3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 8 Oct 2026 16:21:12 +0200 Subject: [PATCH] Tidy the environment, invite and claim surfaces Environment settings show the home date, travel ranges and latest air quality day as formatted dates instead of ISO keys, the backfill card ends in its action row with the status lines above it, and the attribution list sits on the page's reading edge. The sharing invite card keeps a one-sentence description and says once, under the field, that the person needs an account here. An expired claim link offers the sign-in page instead of a dead end. --- .../__tests__/claim-profile-invalid.test.tsx | 40 +++++++++++ src/components/auth/claim-profile.tsx | 11 ++++ .../settings/access/grant-invite-card.tsx | 17 +++-- .../settings/environment-section.tsx | 66 ++++++++++--------- 4 files changed, 95 insertions(+), 39 deletions(-) create mode 100644 src/components/auth/__tests__/claim-profile-invalid.test.tsx diff --git a/src/components/auth/__tests__/claim-profile-invalid.test.tsx b/src/components/auth/__tests__/claim-profile-invalid.test.tsx new file mode 100644 index 000000000..b240065c0 --- /dev/null +++ b/src/components/auth/__tests__/claim-profile-invalid.test.tsx @@ -0,0 +1,40 @@ +import { describe, it, expect, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; + +import { I18nProvider } from "@/lib/i18n/context"; + +/** + * A claim link that no longer works still leaves the page with a way on: + * the card offers the sign-in page instead of ending in a dead end. + */ + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ replace: () => {}, refresh: () => {}, push: () => {} }), +})); +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ user: null }), + clearCachesForSessionEnd: () => {}, +})); +vi.mock("@/lib/queries/use-profile-claim", () => ({ + useProfileClaimPreview: () => ({ data: undefined, error: null }), + useClaimProfile: () => ({ mutate: () => {}, isPending: false }), +})); + +import { ClaimProfile } from "../claim-profile"; + +describe("ClaimProfile — invalid link", () => { + it("offers the sign-in page", () => { + const html = renderToStaticMarkup( + + + + + , + ); + expect(html).toContain('data-testid="claim-invalid"'); + expect(html).toMatch( + /data-slot="claim-to-login"[^>]*href="\/auth\/login"|href="\/auth\/login"[^>]*data-slot="claim-to-login"/, + ); + }); +}); diff --git a/src/components/auth/claim-profile.tsx b/src/components/auth/claim-profile.tsx index 3ddfb1589..c19b25d1b 100644 --- a/src/components/auth/claim-profile.tsx +++ b/src/components/auth/claim-profile.tsx @@ -1,6 +1,7 @@ "use client"; import { useId, useState } from "react"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useQueryClient } from "@tanstack/react-query"; import { KeyRound, Loader2, LogOut } from "lucide-react"; @@ -124,6 +125,16 @@ export function ClaimProfile({ token }: { token: string | null }) { title={t("auth.claim.invalid")} description={t("auth.claim.invalidHint")} /> + {/* A dead link is not a dead end: the way on is the sign-in page. */} + ); } diff --git a/src/components/settings/access/grant-invite-card.tsx b/src/components/settings/access/grant-invite-card.tsx index d9202302c..f563440ae 100644 --- a/src/components/settings/access/grant-invite-card.tsx +++ b/src/components/settings/access/grant-invite-card.tsx @@ -164,14 +164,7 @@ export function GrantInviteCard() {
void submit(event)} className="space-y-3">
@@ -187,8 +180,14 @@ export function GrantInviteCard() { autoComplete="off" maxLength={255} /> + {/* A sharing invitation carries no link and nothing is e-mailed: + the person accepts it from inside their own session. Said right + under the field that names them, so an inviter never goes + looking for a link to send (the invite-link surface is Admin → + Invites, a different thing entirely), and said once — the card + description stays one sentence. */}

- {t("recordSharing.invite.identifierHint")} + {t("recordSharing.invite.needsAccount")}

diff --git a/src/components/settings/environment-section.tsx b/src/components/settings/environment-section.tsx index e607b1542..69c27566e 100644 --- a/src/components/settings/environment-section.tsx +++ b/src/components/settings/environment-section.tsx @@ -21,6 +21,7 @@ import { toast } from "sonner"; import { CloudSun, MapPin, Plane, Trash2, Wind } from "lucide-react"; import { SettingsCard } from "@/components/settings/settings-card"; +import { SettingsCardActions } from "@/components/settings/_card-actions"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { Button } from "@/components/ui/button"; import { DateField } from "@/components/ui/date-field"; @@ -30,7 +31,7 @@ import { Switch } from "@/components/ui/switch"; import { useAuth } from "@/hooks/use-auth"; import { ConfirmButton } from "@/components/ui/confirm-button"; import { formatDateTime } from "@/lib/format"; -import { useTranslations } from "@/lib/i18n/context"; +import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { apiDelete, apiGet, @@ -122,6 +123,7 @@ function envValue(value: number | null, unit: string, digits = 0): string { export function EnvironmentSection() { const { t, tCount } = useTranslations(); + const fmt = useFormatters(); const { user } = useAuth(); const queryClient = useQueryClient(); // Date defaults are the user's own days. The UTC day was tomorrow for @@ -306,7 +308,7 @@ export function EnvironmentSection() { {home.since && (

{t("settings.sections.environment.home.since", { - date: home.since.slice(0, 10), + date: fmt.dateShortSmartCalendar(home.since.slice(0, 10)), })}

)} @@ -393,7 +395,8 @@ export function EnvironmentSection() { className="flex items-center justify-between gap-2 p-2" > - {tr.label} · {tr.startDate} – {tr.endDate} + {tr.label} · {fmt.dateShortSmartCalendar(tr.startDate)} –{" "} + {fmt.dateShortSmartCalendar(tr.endDate)} {t("settings.sections.environment.backfill.description")}

-
+
+
+ {data && ( +

+ {t("settings.sections.environment.storedDays", { + count: data.context.days, + })} +

+ )} + {data?.lastFetchFailure && ( +

+ {tCount( + "settings.sections.environment.fetchFailed", + data.lastFetchFailure.failures, + { + count: data.lastFetchFailure.failures, + when: formatDateTime(data.lastFetchFailure.lastFailedAt), + }, + )} +

+ )} + {/* The action row closes the card; the status lines sit above it. */} + -
- {data && ( -

- {t("settings.sections.environment.storedDays", { - count: data.context.days, - })} -

- )} - {data?.lastFetchFailure && ( -

- {tCount( - "settings.sections.environment.fetchFailed", - data.lastFetchFailure.failures, - { - count: data.lastFetchFailure.failures, - when: formatDateTime(data.lastFetchFailure.lastFailedAt), - }, - )} -

- )} + {/* Air quality, pollen and UV (v1.42) */} @@ -636,7 +642,7 @@ export function EnvironmentSection() {

{t("settings.sections.environment.airQuality.latest", { - date: data.latestDay.date, + date: fmt.dateShortSmartCalendar(data.latestDay.date), })}

@@ -700,7 +706,7 @@ export function EnvironmentSection() { {/* Attribution (CC BY 4.0 for Open-Meteo; Copernicus notice for the air-quality values), one line per source. */}
    {(