Import source: src/components/features/wizard-form
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -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<typeof schema>;
|
||||
|
||||
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<WizardValues>({
|
||||
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 (
|
||||
<Card>
|
||||
<CardContent className="flex min-h-72 flex-col items-center justify-center text-center">
|
||||
<CheckCircle2 className="mb-3 h-10 w-10 text-success" />
|
||||
<h3 className="text-lg font-semibold">{kind} queued successfully</h3>
|
||||
<p className="mt-1 max-w-md text-sm text-muted-foreground">
|
||||
The request was accepted by the CoaxysPanel service layer and the related views are
|
||||
refreshing with the latest control-plane data.
|
||||
</p>
|
||||
<Button className="mt-4" onClick={() => setSubmitted(false)}>
|
||||
Create another
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 lg:grid-cols-[260px_1fr]">
|
||||
<Card className="h-fit">
|
||||
<CardContent className="p-3">
|
||||
<ol className="flex gap-1 overflow-x-auto lg:flex-col lg:overflow-visible">
|
||||
{steps.map((item, index) => {
|
||||
const state = index === step ? "current" : index < step ? "done" : "todo";
|
||||
return (
|
||||
<li key={item} className="shrink-0 lg:shrink">
|
||||
<button
|
||||
className={`focus-ring flex w-full items-center gap-2.5 rounded-md px-3 py-2 text-left text-sm transition-colors ${
|
||||
state === "current"
|
||||
? "bg-primary/10 font-medium text-primary"
|
||||
: "text-muted-foreground hover:bg-muted/70 hover:text-foreground"
|
||||
}`}
|
||||
onClick={() => setStep(index)}
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold transition-colors ${
|
||||
state === "current"
|
||||
? "bg-primary text-primary-foreground shadow-glow-sm"
|
||||
: state === "done"
|
||||
? "bg-success/15 text-success"
|
||||
: "bg-muted text-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
{state === "done" ? <CheckCircle2 className="h-3.5 w-3.5" /> : index + 1}
|
||||
</span>
|
||||
<span className="whitespace-nowrap lg:whitespace-normal">{item}</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{steps[step]}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form
|
||||
className="grid gap-4 lg:grid-cols-2"
|
||||
onSubmit={form.handleSubmit(onSubmit, () => setInvalidSubmit(true))}
|
||||
>
|
||||
<label className="space-y-1 text-sm">
|
||||
<span className="font-medium">Name</span>
|
||||
<Input {...form.register("name")} placeholder={`${kind} name`} />
|
||||
{form.formState.errors.name ? (
|
||||
<span className="text-xs text-destructive">
|
||||
{form.formState.errors.name.message}
|
||||
</span>
|
||||
) : invalidSubmit ? (
|
||||
<span className="text-xs text-destructive">Use at least three characters.</span>
|
||||
) : null}
|
||||
</label>
|
||||
<label className="space-y-1 text-sm">
|
||||
<span className="font-medium">Domain or target</span>
|
||||
<Input {...form.register("domain")} />
|
||||
{form.formState.errors.domain ? (
|
||||
<span className="text-xs text-destructive">
|
||||
{form.formState.errors.domain.message}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<label className="space-y-1 text-sm">
|
||||
<span className="font-medium">Owner</span>
|
||||
<Input {...form.register("owner")} />
|
||||
{form.formState.errors.owner ? (
|
||||
<span className="text-xs text-destructive">
|
||||
{form.formState.errors.owner.message}
|
||||
</span>
|
||||
) : null}
|
||||
</label>
|
||||
<div className="rounded-md border border-border/60 bg-muted/40 p-3 text-sm lg:col-span-2">
|
||||
<div className="mb-2 font-medium">Configuration preview</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge variant="outline">validation enabled</Badge>
|
||||
<Badge variant="outline">mock latency</Badge>
|
||||
<Badge variant="outline">audit metadata</Badge>
|
||||
<Badge variant="outline">permission check</Badge>
|
||||
</div>
|
||||
</div>
|
||||
{submitError ? (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 p-3 text-sm text-destructive lg:col-span-2">
|
||||
{submitError}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="relative z-10 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end lg:col-span-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={step === 0 || mutation.isPending}
|
||||
onClick={() => setStep((value) => value - 1)}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button type="submit" className="w-full sm:w-auto" disabled={mutation.isPending}>
|
||||
{mutation.isPending
|
||||
? "Submitting..."
|
||||
: step === steps.length - 1
|
||||
? "Create"
|
||||
: "Continue"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user