it/web/app/contact/page.tsx
2025-10-26 17:52:17 +01:00

394 lines
15 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
import { Suspense } from "react";
import { useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { motion, AnimatePresence } from "framer-motion";
import { toast } from "sonner";
export default function ContactPage() {
return (
<Suspense fallback={<div className="p-8 text-center">Loading...</div>}>
<RequestForm />
</Suspense>
);
}
function RequestForm() {
const router = useRouter();
const searchParams = useSearchParams();
// Accept both ?type= and ?plan= to allow links from multiple pages
const rawType = (searchParams.get("type") || searchParams.get("plan")) as string | null;
type RequestType =
| "recommendation"
| "vps"
| "website-care"
| "development"
| "minecraft-hosting"
| "plugin"
| "support"
| "partnership";
const isRequestType = (v: any): v is RequestType =>
[
"recommendation",
"vps",
"website-care",
"development",
"minecraft-hosting",
"plugin",
"support",
"partnership",
].includes(v);
const prefillType: RequestType | "" = rawType && isRequestType(rawType) ? rawType : "";
const [step, setStep] = useState(1);
const [loading, setLoading] = useState(false);
const [form, setForm] = useState({
type: prefillType || "",
name: "",
email: "",
domain: "",
company: "",
message: "",
hosting: "", // reused for Minecraft edition (Java/Bedrock/etc.)
concern: "",
});
const requestTypes: { id: RequestType; label: string; desc: string; icon: string }[] = [
{
id: "recommendation",
label: "Get My Recommendation",
desc: "Tell us about your stack/server. Well propose the right plan and next steps.",
icon: "🧭",
},
{
id: "vps",
label: "Managed VPS Hosting",
desc: "Hardened, monitored VPS with restore-tested backups.",
icon: "🖥️",
},
{
id: "website-care",
label: "Managed Website Hosting & Care",
desc: "Updates, uptime, security checks, monthly restore tests.",
icon: "🛡️",
},
{
id: "development",
label: "Website Development",
desc: "Fast, SEO-ready Next.js site with deployment.",
icon: "⚡",
},
{
id: "minecraft-hosting",
label: "Managed Minecraft Hosting",
desc: "Lag-free server on a hardened VPS with backups and tuning.",
icon: "⛏️",
},
{
id: "plugin",
label: "Minecraft Plugin Development",
desc: "Custom Paper/Spigot plugins with tests and docs.",
icon: "🔌",
},
{
id: "support",
label: "Technical Support",
desc: "Report an issue or request hands-on help.",
icon: "🛠️",
},
{
id: "partnership",
label: "Partnership / Collaboration",
desc: "Discuss a potential collaboration or integration.",
icon: "🤝",
},
];
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>
) => {
const { name, value } = e.target;
setForm((f) => ({ ...f, [name]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
const res = await fetch("/api/contact", {
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("Submission failed");
toast.success("Request submitted successfully!");
router.push("/contact/success");
} catch (err) {
console.error(err);
toast.error("Something went wrong. Please try again.");
} finally {
setLoading(false);
}
};
const submitLabel =
form.type === "recommendation"
? "Get My Recommendation"
: form.type === "plugin"
? "Request Plugin Quote"
: form.type === "minecraft-hosting"
? "Launch My Minecraft Server"
: form.type === "development"
? "Request Website Proposal"
: form.type === "vps" || form.type === "website-care"
? "Send Technical Details"
: "Submit Request";
const showDomainInput =
form.type === "recommendation" ||
form.type === "vps" ||
form.type === "website-care" ||
form.type === "development" ||
form.type === "support";
const showMinecraftEdition =
form.type === "minecraft-hosting" || form.type === "plugin";
return (
<main className="relative isolate min-h-screen bg-gradient-to-b from-neutral-50 via-white to-neutral-100 dark:from-neutral-950 dark:via-neutral-900 dark:to-neutral-950">
{/* --- HERO --- */}
<section className="border-b border-neutral-200 dark:border-neutral-800">
<div className="container mx-auto max-w-4xl px-6 py-16 text-center">
<h1 className="text-4xl sm:text-5xl font-bold tracking-tight bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 bg-clip-text text-transparent">
Talk to an Engineer
</h1>
<p className="mt-4 text-neutral-700 dark:text-neutral-300 text-lg">
Tell us about your stack or server. Well recommend a plan and outline next stepsno lock-in.
</p>
{/* Trust microcopy */}
<div className="mt-8 grid grid-cols-1 sm:grid-cols-3 gap-4">
{[
{ t: "Mutual NDA", s: "Available on request" },
{ t: "Least-Privilege Access", s: "Credentials scoped to the task" },
{ t: "Month-to-Month", s: "30-day cancel policy" },
].map((item) => (
<div
key={item.t}
className="rounded-xl border border-neutral-200 dark:border-neutral-800 bg-white/70 dark:bg-neutral-900/70 p-4"
>
<div className="text-sm font-semibold">{item.t}</div>
<div className="text-xs text-neutral-600 dark:text-neutral-400">{item.s}</div>
</div>
))}
</div>
</div>
</section>
{/* --- FORM --- */}
<section className="container mx-auto max-w-3xl px-6 py-12">
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<h2 className="text-2xl font-semibold text-neutral-900 dark:text-white mb-6 text-center">
What do you need help with?
</h2>
<div className="grid gap-6 sm:grid-cols-2">
{requestTypes.map((t) => (
<button
key={t.id}
onClick={() => {
setForm((f) => ({ ...f, type: t.id }));
setStep(2);
}}
className={`rounded-xl border border-neutral-200 dark:border-neutral-800 p-6 text-left hover:shadow-md transition-all duration-200 ${
form.type === t.id
? "bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white"
: "bg-white dark:bg-neutral-900 text-neutral-900 dark:text-white"
}`}
>
<div className="text-3xl mb-2">{t.icon}</div>
<h3 className="font-semibold text-lg mb-1">{t.label}</h3>
<p className="text-sm opacity-80">{t.desc}</p>
</button>
))}
</div>
</motion.div>
)}
{step === 2 && (
<motion.form
key="step2"
onSubmit={handleSubmit}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="mt-4 bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-2xl shadow-sm p-8"
>
<h2 className="text-xl font-semibold mb-6 text-neutral-900 dark:text-white text-center">
{requestTypes.find((t) => t.id === form.type)?.label}
</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Name
</label>
<input
type="text"
name="name"
value={form.name}
onChange={handleChange}
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 focus:outline-none focus:ring-2 focus:ring-blue-500"
required
/>
</div>
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Email
</label>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
required
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 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{showDomainInput && (
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Website or Domain
</label>
<input
type="text"
name="domain"
placeholder="example.com"
value={form.domain}
onChange={handleChange}
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 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
)}
{/* Concern selector for scoping */}
{(form.type !== "" && form.type !== "partnership") && (
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Main Concern
</label>
<select
name="concern"
value={form.concern}
onChange={handleChange}
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 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select one</option>
<option value="speed">Speed / Performance</option>
<option value="security">Security</option>
<option value="dns">DNS / Email Issues</option>
<option value="hosting">Hosting Migration</option>
<option value="minecraft">Minecraft Performance</option>
<option value="plugin">Plugin Feature / Bug</option>
<option value="other">Other</option>
</select>
</div>
)}
{/* Minecraft edition for hosting/plugin */}
{showMinecraftEdition && (
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Minecraft Edition
</label>
<select
name="hosting"
value={form.hosting}
onChange={handleChange}
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 focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Select one</option>
<option value="java">Java (Paper/Spigot)</option>
<option value="bedrock">Bedrock (Geyser supported)</option>
<option value="forge">Modpack / Forge</option>
<option value="fabric">Fabric</option>
<option value="other">Other / Not sure</option>
</select>
</div>
)}
<div>
<label className="block text-sm font-medium mb-1 text-neutral-800 dark:text-neutral-200">
Message / Details
</label>
<textarea
name="message"
rows={4}
value={form.message}
onChange={handleChange}
placeholder={
form.type === "plugin"
? "Describe the plugin features, commands/permissions, and any existing code…"
: form.type === "development"
? "Describe pages/sections, brand goals, timeline, and examples you like…"
: form.type === "minecraft-hosting"
? "Player count, peak times, current host/specs, and performance issues…"
: "Describe your request..."
}
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 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
</div>
<div className="mt-8 flex justify-between items-center">
<button
type="button"
onClick={() => setStep(1)}
className="text-sm text-neutral-600 dark:text-neutral-400 hover:underline"
>
Back
</button>
<button
type="submit"
disabled={loading}
className="rounded-lg bg-gradient-to-r from-blue-600 via-purple-600 to-blue-600 text-white font-medium px-6 py-2 hover:opacity-90 transition"
>
{loading ? "Submitting..." : submitLabel}
</button>
</div>
{/* GDPR-friendly footer note */}
<p className="mt-4 text-xs text-neutral-600 dark:text-neutral-400 text-center">
We use your details only to respond to your request. No unsolicited marketing. EU/GDPR-friendly.
</p>
</motion.form>
)}
</AnimatePresence>
</section>
</main>
);
}