diff --git a/src/components/features/wizard-form.tsx b/src/components/features/wizard-form.tsx new file mode 100644 index 0000000..dbba96c --- /dev/null +++ b/src/components/features/wizard-form.tsx @@ -0,0 +1,211 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { CheckCircle2 } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Input } from "@/components/ui/input"; +import { Badge } from "@/components/ui/badge"; +import { panelService } from "@/api/services/panel-service"; + +const schema = z.object({ + name: z.string().min(3, "Use at least three characters."), + domain: z.string().min(4, "Enter a valid domain or identifier."), + owner: z.string().min(2, "Owner is required."), +}); + +type WizardValues = z.infer; + +export function WizardForm({ kind, steps }: { kind: string; steps: string[] }) { + const queryClient = useQueryClient(); + const [step, setStep] = useState(0); + const [submitted, setSubmitted] = useState(false); + const [invalidSubmit, setInvalidSubmit] = useState(false); + const [submitError, setSubmitError] = useState(""); + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { name: "", domain: "example.com", owner: "coaxys" }, + }); + + const mutation = useMutation({ + mutationFn: async (values: WizardValues) => { + if (kind === "website") { + return panelService.websites.create({ + name: values.name, + domain: values.domain, + runtime: "PHP", + environment: "production", + }); + } + if (kind === "database") return panelService.databases.create(values.name); + if (kind === "backup") return panelService.backups.create(values.name); + return panelService.files.operation(`${kind}: ${values.name}`); + }, + onSuccess: async (response) => { + if (response.error) { + setSubmitError(response.error.message); + for (const [field, message] of Object.entries(response.error.fields ?? {})) { + if (field === "name" || field === "domain" || field === "owner") { + form.setError(field, { message }); + } + } + return; + } + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ["dashboard-summary"] }), + queryClient.invalidateQueries({ queryKey: ["websites"] }), + queryClient.invalidateQueries({ queryKey: ["databases"] }), + queryClient.invalidateQueries({ queryKey: ["backups"] }), + ]); + setSubmitted(true); + }, + onError: (error) => { + setSubmitError(error instanceof Error ? error.message : "The request failed."); + }, + }); + + function onSubmit(values: WizardValues) { + setInvalidSubmit(false); + setSubmitError(""); + if (step < steps.length - 1) { + setStep((value) => value + 1); + return; + } + mutation.mutate(values); + } + + if (submitted) { + return ( + + + +

{kind} queued successfully

+

+ The request was accepted by the CoaxysPanel service layer and the related views are + refreshing with the latest control-plane data. +

+ +
+
+ ); + } + + return ( +
+ + +
    + {steps.map((item, index) => { + const state = index === step ? "current" : index < step ? "done" : "todo"; + return ( +
  1. + +
  2. + ); + })} +
+
+
+ + + {steps[step]} + + +
setInvalidSubmit(true))} + > + + + +
+
Configuration preview
+
+ validation enabled + mock latency + audit metadata + permission check +
+
+ {submitError ? ( +
+ {submitError} +
+ ) : null} +
+ + +
+
+
+
+
+ ); +}