138 lines
5.3 KiB
TypeScript
138 lines
5.3 KiB
TypeScript
import clsx from "clsx";
|
|
import ButtonSecondary from "../buttons/ButtonSecondary";
|
|
import LoadingButton from "../buttons/LoadingButton";
|
|
import { useTranslation } from "react-i18next";
|
|
import { ChevronLeftIcon, ChevronRightIcon } from "~/components/ui/icons/vendor";
|
|
|
|
export type WizardStepDto = {
|
|
name: string;
|
|
description?: string;
|
|
};
|
|
|
|
interface Props {
|
|
steps: WizardStepDto[];
|
|
selectedStep: number;
|
|
onSetStep: (step: number, direction: "next" | "back" | undefined) => void;
|
|
children: React.ReactNode;
|
|
breakpoint?: "sm" | "md" | "lg" | "xl" | "2xl";
|
|
className?: string;
|
|
}
|
|
|
|
export default function WizardSteps({ steps, selectedStep, onSetStep, children, className, breakpoint = "md" }: Props) {
|
|
const { t } = useTranslation();
|
|
const canGoBack = selectedStep > 0;
|
|
const canGoNext = selectedStep < steps.length - 1;
|
|
const backTitle = canGoBack ? steps[selectedStep - 1].name : t("shared.back");
|
|
const nextTitle = canGoNext ? steps[selectedStep + 1].name : t("shared.next");
|
|
function onBack() {
|
|
if (canGoBack) {
|
|
onSetStep(selectedStep - 1, "back");
|
|
}
|
|
}
|
|
function onNext() {
|
|
if (canGoNext) {
|
|
onSetStep(selectedStep + 1, "next");
|
|
}
|
|
}
|
|
return (
|
|
<div className="relative">
|
|
<div
|
|
className={clsx(
|
|
"p-1",
|
|
breakpoint === "sm" && "sm:hidden",
|
|
breakpoint === "md" && "md:hidden",
|
|
breakpoint === "lg" && "lg:hidden",
|
|
breakpoint === "xl" && "xl:hidden",
|
|
breakpoint === "2xl" && "2xl:hidden"
|
|
)}
|
|
>
|
|
<label htmlFor="tabs" className="sr-only">
|
|
{t("shared.select")}
|
|
</label>
|
|
<select
|
|
id="tabs"
|
|
name="tabs"
|
|
className="border-border-default bg-surface-neutral-primary text-text-neutral-primary focus:border-border-default focus:shadow-focus block w-full rounded-md"
|
|
onChange={(e) => onSetStep(Number(e.target.value), undefined)}
|
|
value={selectedStep}
|
|
>
|
|
{steps.map((item, idx) => {
|
|
return (
|
|
<option key={item.name} value={Number(idx)}>
|
|
{item.name}
|
|
</option>
|
|
);
|
|
})}
|
|
</select>
|
|
</div>
|
|
<div
|
|
className={clsx(
|
|
className,
|
|
breakpoint === "sm" && "sm:flex sm:space-x-4",
|
|
breakpoint === "md" && "md:flex md:space-x-4",
|
|
breakpoint === "lg" && "lg:flex lg:space-x-4",
|
|
breakpoint === "xl" && "xl:flex xl:space-x-4",
|
|
breakpoint === "2xl" && "2xl:flex 2xl:space-x-4"
|
|
)}
|
|
>
|
|
<div className="hidden overflow-auto p-3 md:block md:w-1/4">
|
|
<nav aria-label={t("shared.progress")}>
|
|
<ol className="overflow-hidden">
|
|
{steps.map((step, stepIdx) => (
|
|
<li key={step.name} className={clsx(stepIdx !== steps.length - 1 ? "pb-10" : "", "relative")}>
|
|
{stepIdx !== steps.length - 1 ? <div className="absolute left-3 top-4 -ml-px mt-0.5 h-full w-0.5 bg-gray-300" aria-hidden="true" /> : null}
|
|
<button type="button" onClick={() => onSetStep(stepIdx, undefined)} className="group relative flex items-start text-left">
|
|
<span className="flex h-9 items-center" aria-hidden="true">
|
|
<span
|
|
className={clsx(
|
|
"border-border-default bg-surface-neutral-primary relative flex h-6 w-6 items-center justify-center rounded-full border-2",
|
|
stepIdx === selectedStep ? "border-theme-600 group-hover:border-theme-600" : "group-hover:border-theme-600"
|
|
)}
|
|
>
|
|
{stepIdx === selectedStep ? (
|
|
<span className="bg-theme-600 h-2.5 w-2.5 rounded-full" />
|
|
) : (
|
|
<span className="group-hover:bg-theme-600 h-2.5 w-2.5 rounded-full bg-gray-400" />
|
|
)}
|
|
</span>
|
|
</span>
|
|
<span className="ml-4 flex min-w-0 flex-col">
|
|
<span className="text-text-neutral-tertiary text-sm font-medium">{step.name}</span>
|
|
<span className="text-text-neutral-tertiary line-clamp-2 text-sm">{step.description}</span>
|
|
</span>
|
|
</button>
|
|
</li>
|
|
))}
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div className="h-full w-full overflow-auto pb-20 md:w-3/4">{children}</div>
|
|
</div>
|
|
<div className="bg-surface-neutral-secondary/90 sticky bottom-0 -mx-8 px-4 py-3 shadow-inner md:py-2">
|
|
<div className="flex w-full justify-between space-x-2">
|
|
<div>
|
|
{canGoBack && (
|
|
<ButtonSecondary onClick={onBack}>
|
|
<div className="flex items-center space-x-2">
|
|
<ChevronLeftIcon className="h-4 w-4" />
|
|
</div>
|
|
<div>{backTitle}</div>
|
|
</ButtonSecondary>
|
|
)}
|
|
</div>
|
|
<div>
|
|
{canGoNext && (
|
|
<LoadingButton onClick={onNext}>
|
|
<div className="flex items-center space-x-2">
|
|
<div>{nextTitle}</div>
|
|
<ChevronRightIcon className="h-4 w-4" />
|
|
</div>
|
|
</LoadingButton>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|