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. */}
    {(