"use client"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { AnimatePresence, motion } from "framer-motion"; import { toast } from "sonner"; type PlanId = "solo" | "team" | "dedicated"; type Region = "eu-nl" | "eu-de" | "us-east" | "us-west"; type OSImage = "ubuntu-24-04" | "ubuntu-22-04" | "debian-12"; type OwnershipMode = "managed" | "owned"; type Plan = { id: PlanId; name: string; price: string; specs: string; bestFor: string; features: readonly string[]; }; type RegionOpt = { id: Region; label: string }; type OSOpt = { id: OSImage; label: string }; export default function VPSDeployWizard({ plans, regions, osImages, siteHostname, }: { plans: readonly Plan[]; regions: readonly RegionOpt[]; osImages: readonly OSOpt[]; siteHostname: string; }) { const router = useRouter(); const [step, setStep] = useState(1); const [loading, setLoading] = useState(false); const [form, setForm] = useState<{ plan: PlanId | ""; region: Region | ""; osImage: OSImage | ""; hostname: string; fqdn: string; sshPublicKey: string; ownership: OwnershipMode; provider: "" | "hetzner" | "digitalocean" | "vultr"; email: string; notes: string; acceptTerms: boolean; enableBackups: boolean; }>({ plan: "", region: "", osImage: "ubuntu-24-04", hostname: "", fqdn: "", sshPublicKey: "", ownership: "managed", provider: "", email: "", notes: "", acceptTerms: false, enableBackups: true, }); const set = (key: K, value: (typeof form)[K]) => setForm((f) => ({ ...f, [key]: value })); // Preselect plan from hash/query if present if (typeof window !== "undefined" && !form.plan) { const hash = window.location.hash; const url = new URL(window.location.href); const qp = url.searchParams.get("plan") as PlanId | null; const fromHash = /plan=(solo|team|dedicated)/.exec(hash || "")?.[1] as PlanId | undefined; const initial = qp || fromHash; if (initial) set("plan", initial); } const canContinue1 = !!form.plan; const canContinue2 = !!form.region && !!form.osImage; const canSubmit = canContinue2 && !!form.hostname && !!form.email && form.acceptTerms && (form.ownership === "managed" || (form.ownership === "owned" && !!form.provider)); const handleSubmit = async () => { if (!canSubmit) return; setLoading(true); try { const res = await fetch("/api/vps/create", { method: "POST", headers: { "Content-Type": "application/json", "x-secret-token": process.env.NEXT_PUBLIC_FORM_SECRET || "", }, body: JSON.stringify(form), }); if (!res.ok) throw new Error("Provisioning failed"); toast.success( form.ownership === "owned" ? "Owned VPS request received! Check your email to securely connect your provider." : "VPS provisioning started! Check your email for access details." ); router.push("/vps/success"); } catch (err) { console.error(err); toast.error("Something went wrong. Please try again."); } finally { setLoading(false); } }; return (
{step === 1 && (

1) Choose a Plan

You can change plans later. Dedicated is custom—contact us for sizing.

{plans.map((p) => ( ))}
)} {step === 2 && (

2) Configure

{/* Region */}
{/* OS */}
{/* Hostname */}
set("hostname", e.target.value.replace(/[^a-z0-9-.]/gi, "").toLowerCase())} placeholder="app-1" className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2" />
{/* FQDN */}
set("fqdn", e.target.value.toLowerCase())} placeholder={`app.${siteHostname}`} className="w-full rounded-lg border border-neutral-300 dark:border-neutral-700 bg-white/80 dark:bg-neutral-900/80 px-3 py-2" />

We can map a subdomain like app.{siteHostname}.

{/* SSH Public Key */}