Fix Multi-Step-Wizard

Implement generic Wizard component for future use in Gameyfin
This commit is contained in:
grimsi
2024-03-16 14:48:06 +01:00
parent 0b696e4766
commit 4ad0914b17
6 changed files with 185 additions and 150 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="UI debug" type="JavascriptDebugType" engineId="37cae5b9-e8b2-4949-9172-aafa37fbc09c" uri="http://localhost:8080" useFirstLineBreakpoints="true">
<configuration default="false" name="UI debug" type="JavascriptDebugType" uri="http://localhost:8080" useFirstLineBreakpoints="true">
<method v="2" />
</configuration>
</component>
+1 -18
View File
@@ -30,21 +30,4 @@ const Input = ({label, ...props}) => {
);
}
export default Input;
/*
<Input
onChange={(event) => {
setUsername(event.target.value);
}}
id="username"
type="text"
autoComplete="username"
placeholder=""
size="lg"
className=" !border-t-blue-gray-200 focus:!border-t-gray-900"
labelProps={{
className: "before:content-none after:content-none",
}}
crossOrigin="" //TODO: see https://github.com/creativetimofficial/material-tailwind/issues/427
/>
*/
export default Input;
+87
View File
@@ -0,0 +1,87 @@
import React, {useState} from "react";
import {Form, Formik, FormikBag, FormikHelpers} from "formik";
import {Button, Spinner, Step, Stepper} from "@material-tailwind/react";
import {ArrowLeft, ArrowRight, Check} from "@phosphor-icons/react";
const Wizard = ({children, initialValues, onSubmit}: { children: any, initialValues: any, onSubmit: any }) => {
const [stepNumber, setStepNumber] = useState(0);
const steps = React.Children.toArray(children);
const [snapshot, setSnapshot] = useState(initialValues);
const step = steps[stepNumber];
const totalSteps = steps.length;
const isFirstStep = stepNumber === 0;
const isLastStep = stepNumber === totalSteps - 1;
const next = (values: any) => {
setSnapshot(values);
setStepNumber(Math.min(stepNumber + 1, totalSteps - 1));
};
const previous = (values: any) => {
setSnapshot(values);
setStepNumber(Math.max(stepNumber - 1, 0));
};
const handleSubmit = async (values: any, bag: FormikBag<any, any> | FormikHelpers<any>) => {
/*// @ts-ignore*/
if (step.props.onSubmit) {
/*// @ts-ignore*/
await step.props.onSubmit(values, bag);
}
if (isLastStep) {
return onSubmit(values, bag);
} else {
await bag.setTouched({});
next(values);
}
};
return (
<Formik
initialValues={snapshot}
onSubmit={handleSubmit}
isInitialValid={false}
/*// @ts-ignore*/
validationSchema={step.props.validationSchema}
>
{formik => (
<Form className="flex flex-col grow">
<div className="w-full mb-8">
<Stepper
activeStep={stepNumber}
>
{steps.map((child, index) => (
<Step key={index}>
{/*// @ts-ignore*/}
{child.props.icon}
</Step>
))}
</Stepper>
</div>
<div className="flex grow">
{step}
</div>
<div className="bottom-0 w-full">
<div className="flex justify-between">
<Button onClick={() => previous(formik.values)} disabled={isFirstStep}
className="rounded-full">
<ArrowLeft/>
</Button>
<Button disabled={formik.isSubmitting}
className="rounded-full"
type="submit"
>
{formik.isSubmitting ?
<Spinner className="h-5 w-5"/> : isLastStep ? <Check/> : <ArrowRight/>
}
</Button>
</div>
</div>
</Form>
)}
</Formik>
);
};
export default Wizard;
@@ -0,0 +1,3 @@
const WizardStep = ({children}: { children: any }) => children;
export default WizardStep;
+93 -131
View File
@@ -1,55 +1,89 @@
import {Button, Card, Spinner, Step, Stepper, Typography} from "@material-tailwind/react";
import {useState} from "react";
import {ArrowLeft, ArrowRight, Check, GearFine, HandWaving, User} from "@phosphor-icons/react";
import {Form, Formik} from "formik";
import React from 'react';
import {useFormikContext} from 'formik';
import * as Yup from 'yup';
import Wizard from "Frontend/components/wizard/Wizard";
import WizardStep from "Frontend/components/wizard/WizardStep";
import {Card, Typography} from "@material-tailwind/react";
import Input from "Frontend/components/Input";
import {GearFine, HandWaving, User} from "@phosphor-icons/react";
export default function SetupView() {
const [activeStep, setActiveStep] = useState(0);
const [isLastStep, setIsLastStep] = useState(false);
const [isFirstStep, setIsFirstStep] = useState(false);
const [isLoading, setLoading] = useState(false);
const [isNextDisabled, setIsNextDisabled] = useState(false);
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
const steps = [<WelcomeStep/>, <UserStep/>, <SettingsStep/>];
function WelcomeStep() {
return (
<div className="flex flex-col size-full gap-12 items-center">
<Typography variant="h4">Welcome to Gameyfin 👋</Typography>
<p className="place-content-center text-justify">
Gameyfin is a cutting-edge software tailored for gamers seeking efficient management of their
video
game collections. <br/><br/> With its intuitive interface and comprehensive features, Gameyfin
simplifies the organization of game libraries. Users can effortlessly add games through manual
input
or
automated recognition, categorize them based on various criteria like genre or platform, track
in-game
progress, and share achievements with friends. <br/><br/> Notably, Gameyfin stands out for its
user-friendly
design and adaptability, offering ample customization options to meet diverse user preferences.
</p>
<Typography variant="h5">Let's get started!</Typography>
</div>
);
}
const handleNext = () => !isLastStep && setActiveStep((cur) => cur + 1);
const handlePrev = () => !isFirstStep && setActiveStep((cur) => cur - 1);
const finish = () => {
alert("Setup finished");
}
function WelcomeStep() {
return (
<div className="flex flex-col size-full gap-12 items-center">
<Typography variant="h4">Welcome to Gameyfin 👋</Typography>
<p className="place-content-center text-justify">
Gameyfin is a cutting-edge software tailored for gamers seeking efficient management of their
video
game collections. <br/><br/> With its intuitive interface and comprehensive features, Gameyfin
simplifies the organization of game libraries. Users can effortlessly add games through manual
input
or
automated recognition, categorize them based on various criteria like genre or platform, track
in-game
progress, and share achievements with friends. <br/><br/> Notably, Gameyfin stands out for its
user-friendly
design and adaptability, offering ample customization options to meet diverse user preferences.
</p>
<Typography variant="h5">Let's get started!</Typography>
function UserStep() {
const formik = useFormikContext();
return (
<div className="flex flex-col size-full gap-12 items-center">
<Typography variant="h4">Create your account</Typography>
<Typography className="-mt-8">This will set up the initial admin user account.</Typography>
<div className="mb-1 flex flex-col w-full gap-6">
<Input
label="Username"
name="username"
type="text"
/>
<Input
label="Password"
name="password"
type="password"
/>
<Input
label="Password (repeat)"
name="passwordRepeat"
type="password"
/>
</div>
);
}
</div>
);
}
function UserStep() {
return (
<div className="flex flex-col size-full gap-12 items-center">
<Typography variant="h4">Create your account</Typography>
<Typography className="-mt-8">This will set up the initial admin user account.</Typography>
<Formik
validateOnMount
initialValues={{username: '', password: '', passwordRepeat: ''}}
function SettingsStep() {
return (
<div className="flex flex-col items-center">
<Typography variant="h4">Settings</Typography>
<Typography>Configure your settings</Typography>
</div>
);
}
const SetupView = () => (
<div className="flex h-screen">
<div className="fixed size-full bg-gradient-to-br from-gf-primary to-gf-secondary"></div>
<Card className="w-3/4 h-3/4 min-w-[500px] m-auto p-8" shadow={true}>
<Wizard
initialValues={{username: '', password: '', passwordRepeat: ''}}
onSubmit={async (values: any) =>
sleep(300).then(() => alert(JSON.stringify(values, null, 2)))
}
>
<WizardStep
// @ts-ignore
icon={<HandWaving/>}>
<WelcomeStep/>
</WizardStep>
<WizardStep
// @ts-ignore
validationSchema={Yup.object({
username: Yup.string()
.required('Required'),
@@ -60,91 +94,19 @@ export default function SetupView() {
.equals([Yup.ref('password')], 'Passwords do not match')
.required('Required')
})}
onSubmit={(values, {setSubmitting}) => {
setTimeout(() => {
alert(JSON.stringify(values, null, 2));
setSubmitting(false);
}, 400);
}}
icon={<User/>}
>
{formik =>
<Form className="mb-1 flex flex-col w-full gap-6">
<Input
label="Username"
name="username"
type="text"
/>
<Input
label="Password"
name="password"
type="password"
/>
<Input
label="Password (repeat)"
name="passwordRepeat"
type="password"
/>
<Button
type="submit"
disabled={!formik.isValid || formik.isSubmitting}
className="justify-center"
>
{formik.isSubmitting ?
<Spinner className="h-4 w-full"/> : "Submit"
}
</Button>
</Form>
}
</Formik>
</div>
);
}
<UserStep/>
</WizardStep>
<WizardStep
//@ts-ignore
icon={<GearFine/>}
>
<SettingsStep/>
</WizardStep>
</Wizard>
</Card>
</div>
);
function SettingsStep() {
return (
<>
</>
);
}
return (
<div className="flex h-screen">
<div className="fixed size-full bg-gradient-to-br from-gf-primary to-gf-secondary"></div>
<Card className="w-3/4 h-3/4 min-w-[500px] m-auto p-8" shadow={true}>
<div className="w-full mb-8">
<Stepper
activeStep={activeStep}
isLastStep={(value) => setIsLastStep(value)}
isFirstStep={(value) => setIsFirstStep(value)}
>
<Step onClick={() => setActiveStep(0)}>
<HandWaving/>
</Step>
<Step onClick={() => setActiveStep(1)}>
<User/>
</Step>
<Step onClick={() => setActiveStep(2)}>
<GearFine/>
</Step>
</Stepper>
</div>
<div className="flex flex-grow justify-center">
<div className="size-full px-8 min-w-[500px] md:w-1/3 md:px-0">
{steps[activeStep]}
</div>
</div>
<div className="bottom-0 w-full">
<div className="flex justify-between">
<Button onClick={handlePrev} disabled={isFirstStep} className="rounded-full">
<ArrowLeft/>
</Button>
<Button disabled={isNextDisabled} onClick={isLastStep ? finish : handleNext}
className="rounded-full">
{isLoading ? <Spinner/> : isLastStep ? <Check/> : <ArrowRight/>}
</Button>
</div>
</div>
</Card>
</div>
);
}
export default SetupView;
Vendored Regular → Executable
View File