394 lines
15 KiB
TypeScript
394 lines
15 KiB
TypeScript
"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. We’ll 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. We’ll recommend a plan and outline next steps—no 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>
|
||
);
|
||
}
|