Simpl-Resultat/src/components/shared/PageHelp.tsx
le king fu 624eef7ba8
All checks were successful
PR Check — Frontend / frontend (pull_request) Successful in 1m59s
feat(onboarding): Import empty states, sample folder button and guide links (#356)
The import page without a source no longer shows a bare "no source found"
card. `ImportEmptyState` (built on the shared `EmptyState`) takes the place
of the empty `SourceList` in both situations: without an import folder it
offers "Choose a folder" as the single primary action; with a folder and no
bank sub-folder it offers "Create a sample folder" (primary, disabled while
in flight) and "Refresh" (secondary). Both variants, and the "?" help
popover through the new optional `linkTo` prop of `PageHelp`, link to the
bank CSV export guide at `/settings/users#import-export`. `SourceList`
stays an untouched pure list.

`useImportWizard` gains `createSampleFolders`: it invokes the Rust command
`create_sample_import_folders` with the folder and the webview's LOCAL date
(built from the local getters, not `toISOString()`), rescans the folder so
the two cards appear without another click, then raises a closable success
banner from the new `sampleCreated` state, which `reset` and a folder change
clear. A refusal from Rust (a sample folder already exists) lands in the
existing error banner. The log line carries the folder NAMES only, never
the absolute path, since the logs travel with a feedback report.

"Browse" on the folder card drops to the secondary style while no folder is
configured, so a single primary button is on screen. The reducer and its
initial state are exported as `wizardReducer` / `initialWizardState` and
unit-tested; `ImportEmptyState` is tested by direct call.

Resolves #356

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 20:10:25 -04:00

78 lines
2.7 KiB
TypeScript

import { useState, useRef, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import { CircleHelp, X } from "lucide-react";
interface PageHelpProps {
helpKey: string;
/**
* Optional route rendered as a link under the tips (#356), labelled by
* `${helpKey}.help.linkLabel`. No close handler on click: navigating away
* unmounts the page and the popover with it.
*/
linkTo?: string;
}
export function PageHelp({ helpKey, linkTo }: PageHelpProps) {
const { t } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
// Close on outside click (same pattern as CategoryCombobox)
useEffect(() => {
if (!isOpen) return;
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, [isOpen]);
const tips = t(`${helpKey}.help.tips`, { returnObjects: true }) as string[];
return (
<div ref={ref} className="relative">
<button
onClick={() => setIsOpen(!isOpen)}
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors"
aria-label={t(`${helpKey}.help.title`)}
>
<CircleHelp size={20} />
</button>
{isOpen && (
<div className="absolute left-0 top-full mt-2 z-40 w-[calc(100vw-var(--sidebar-width,16rem)-6rem)] max-w-3xl bg-[var(--card)] border border-[var(--border)] rounded-xl p-5 shadow-lg">
<div className="flex items-start justify-between gap-4 mb-3">
<h3 className="font-semibold text-[var(--foreground)]">
{t(`${helpKey}.help.title`)}
</h3>
<button
onClick={() => setIsOpen(false)}
className="text-[var(--muted-foreground)] hover:text-[var(--foreground)] transition-colors shrink-0"
>
<X size={18} />
</button>
</div>
<ul className="space-y-1.5 text-sm text-[var(--muted-foreground)]">
{Array.isArray(tips) &&
tips.map((tip, i) => (
<li key={i} className="flex gap-2">
<span className="shrink-0">•</span>
<span>{tip}</span>
</li>
))}
</ul>
{linkTo && (
<Link
to={linkTo}
className="inline-block mt-3 text-sm text-[var(--primary)] hover:underline"
>
{t(`${helpKey}.help.linkLabel`)}
</Link>
)}
</div>
)}
</div>
);
}