All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m46s
A profile without any transaction now opens a dashboard that says what to do: a welcome card with the expanded getting-started checklist and one primary action towards the import page, instead of widgets showing 0 $. With transactions, the compact checklist sits between the title and the filter panel until it is complete or dismissed. - `EmptyState` (pure, dashed card from the import source list; a `to` action renders a react-router `<Link>`, an `onClick` action a button) - `OnboardingChecklist` (pure, expanded and compact modes; undone rows link to their route, the locked Budget row shows a lock, the "Base and up" label and links to the license card; neutral budget row and hidden counter until the entitlement is known) - `DashboardEmptyState` composes both - `DashboardPage` branches on the hook `status`: loading renders the title, help and banner only; error falls back to the v0.15.0 page without the list and logs a warning; ready with zero transactions renders the empty state outside the `isLoading` veil - `useOnboardingChecklist` exposes `counters` and `error` for the page - Users settings: "Show the getting-started checklist" button in the Guide section, visible only when the preference is set, with an inline confirmation after the click - FR + EN keys `dashboard.empty.*`, `onboarding.checklist.*`, `settings.users.onboarding.*` Tests: `EmptyState.test.tsx` and `OnboardingChecklist.test.tsx` by direct call (FilterPanel pattern). No DB migration. CHANGELOG left to #357. Resolves #352 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { Link } from "react-router-dom";
|
|
import { ArrowLeft, ListChecks } from "lucide-react";
|
|
import AccountCard from "../../components/settings/AccountCard";
|
|
import LicenseCard from "../../components/settings/LicenseCard";
|
|
import DocsContent from "../../components/settings/DocsContent";
|
|
import { getPreference } from "../../services/userPreferenceService";
|
|
import {
|
|
ONBOARDING_CHECKLIST_HIDDEN_KEY,
|
|
showChecklist,
|
|
} from "../../services/onboardingService";
|
|
import { logError } from "../../services/logService";
|
|
|
|
// Re-show button of the getting-started checklist (#352). `hidden` = the
|
|
// preference was "1" at mount; `shown` = the button was clicked and the
|
|
// preference removed, so a confirmation line replaces the button.
|
|
type ChecklistButtonState = "unknown" | "visible" | "hidden" | "shown";
|
|
|
|
export default function UsersSettingsPage() {
|
|
const { t } = useTranslation();
|
|
const [checklistButton, setChecklistButton] = useState<ChecklistButtonState>("unknown");
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
(async () => {
|
|
try {
|
|
const value = await getPreference(ONBOARDING_CHECKLIST_HIDDEN_KEY);
|
|
if (!cancelled) setChecklistButton(value === "1" ? "hidden" : "visible");
|
|
} catch (e) {
|
|
// Informational control: without the preference, keep it out of the way.
|
|
if (!cancelled) setChecklistButton("visible");
|
|
logError("Settings: failed to read the checklist preference", e);
|
|
}
|
|
})();
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, []);
|
|
|
|
const handleShowChecklist = async () => {
|
|
try {
|
|
await showChecklist();
|
|
setChecklistButton("shown");
|
|
} catch (e) {
|
|
logError("Settings: failed to show the checklist", e);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<Link
|
|
to="/settings"
|
|
className="inline-flex items-center gap-2 text-sm text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
|
|
>
|
|
<ArrowLeft size={16} />
|
|
{t("settings.backToHome")}
|
|
</Link>
|
|
|
|
<h1 className="text-2xl font-bold">{t("settings.users.title")}</h1>
|
|
|
|
<section className="space-y-3">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)]">
|
|
{t("settings.users.sections.accounts")}
|
|
</h2>
|
|
<AccountCard />
|
|
</section>
|
|
|
|
<section className="space-y-3">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)]">
|
|
{t("settings.users.sections.licenses")}
|
|
</h2>
|
|
<LicenseCard />
|
|
</section>
|
|
|
|
<section className="space-y-3">
|
|
<h2 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted-foreground)]">
|
|
{t("settings.users.sections.userGuide")}
|
|
</h2>
|
|
{(checklistButton === "hidden" || checklistButton === "shown") && (
|
|
<div className="flex flex-wrap items-center gap-3 rounded-xl border border-[var(--border)] bg-[var(--card)] p-4 text-sm">
|
|
<ListChecks size={18} className="shrink-0 text-[var(--muted-foreground)]" aria-hidden="true" />
|
|
<span className="flex-1 min-w-[12rem]">{t("settings.users.onboarding.label")}</span>
|
|
{checklistButton === "hidden" ? (
|
|
<button
|
|
type="button"
|
|
onClick={handleShowChecklist}
|
|
className="inline-flex items-center justify-center px-3 py-1.5 rounded-md border border-[var(--border)] font-medium hover:bg-[var(--muted)] transition-colors"
|
|
>
|
|
{t("settings.users.onboarding.show")}
|
|
</button>
|
|
) : (
|
|
<span role="status" className="text-[var(--muted-foreground)]">
|
|
{t("settings.users.onboarding.confirmation")}
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
<DocsContent />
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|