GRV-Summit-Site/components/payment/PaymentForm.tsx
kirukib cb404ec079
Some checks failed
Deploy to Cloudflare Workers (OpenNext) / deploy (push) Has been cancelled
Align site colors with GRV brand book palette.
Centralize primary, secondary, tertiary, and neutral tokens and apply them across theme variables and UI components.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:45:22 +03:00

216 lines
7.4 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";
import { useMemo, useState } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { ticketTiers, paymentMethods } from "@/content/tickets";
import { calculateTotal } from "@/lib/payment";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { DataConsentField } from "@/components/forms/DataConsentField";
import { dataConsent } from "@/content/consent";
import { cn } from "@/lib/utils";
export function PaymentForm() {
const searchParams = useSearchParams();
const router = useRouter();
const initialTicket = searchParams.get("ticket") ?? ticketTiers[0].id;
const [ticketId, setTicketId] = useState(initialTicket);
const [quantity, setQuantity] = useState(1);
const [paymentMethod, setPaymentMethod] = useState<"card" | "bank">("card");
const [status, setStatus] = useState<"idle" | "loading" | "error">("idle");
const [error, setError] = useState("");
const [consent, setConsent] = useState(false);
const tier = ticketTiers.find((t) => t.id === ticketId) ?? ticketTiers[0];
const total = useMemo(() => calculateTotal(ticketId, quantity), [ticketId, quantity]);
async function onSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
if (!consent) {
setError(dataConsent.errorMessage);
return;
}
setStatus("loading");
setError("");
const form = e.currentTarget;
const data = new FormData(form);
try {
const res = await fetch("/api/payment", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
ticketId,
quantity,
paymentMethod,
name: data.get("name"),
email: data.get("email"),
company: data.get("company") || undefined,
phone: data.get("phone") || undefined,
consent: true,
}),
});
const json = await res.json();
if (!res.ok || !json.ok) {
throw new Error(json.error || "Payment failed");
}
router.push(`/payment/success?order=${json.orderId}&total=${json.totalUsd}`);
} catch (err) {
setStatus("error");
setError(err instanceof Error ? err.message : "Payment failed");
}
}
return (
<form onSubmit={onSubmit} className="grid gap-8 lg:grid-cols-5">
<div className="space-y-4 lg:col-span-3">
<div className="space-y-2">
<Label>Ticket type</Label>
<div className="grid gap-3 sm:grid-cols-3">
{ticketTiers.map((t) => (
<button
key={t.id}
type="button"
disabled={t.soldOut}
onClick={() => setTicketId(t.id)}
className={cn(
"rounded-xl border p-4 text-left transition-colors",
ticketId === t.id
? "border-[#30614c] bg-[#30614c]/5 ring-2 ring-[#30614c]"
: "border-border hover:border-[#30614c]/40",
t.soldOut && "opacity-50"
)}
>
<p className="font-semibold">{t.name}</p>
<p className="mt-1 text-lg font-bold text-[#30614c]">${t.priceUsd}</p>
{t.soldOut && <p className="text-xs text-destructive">Sold out</p>}
</button>
))}
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="quantity">Quantity</Label>
<Select
value={String(quantity)}
onValueChange={(v) => setQuantity(Number(v))}
>
<SelectTrigger id="quantity">
<SelectValue />
</SelectTrigger>
<SelectContent>
{[1, 2, 3, 4, 5].map((n) => (
<SelectItem key={n} value={String(n)}>
{n}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label>Payment method</Label>
<div className="grid gap-2 sm:grid-cols-2">
{paymentMethods.map((m) => (
<button
key={m.id}
type="button"
onClick={() => setPaymentMethod(m.id)}
className={cn(
"rounded-xl border p-4 text-left",
paymentMethod === m.id
? "border-[#37a47a] ring-2 ring-[#37a47a]"
: "border-border"
)}
>
<p className="font-medium">{m.label}</p>
<p className="text-sm text-muted-foreground">{m.description}</p>
</button>
))}
</div>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="name">Full name</Label>
<Input id="name" name="name" required />
</div>
<div className="space-y-2">
<Label htmlFor="email">Email</Label>
<Input id="email" name="email" type="email" required />
</div>
<div className="space-y-2">
<Label htmlFor="company">Company (optional)</Label>
<Input id="company" name="company" />
</div>
<div className="space-y-2">
<Label htmlFor="phone">Phone (optional)</Label>
<Input id="phone" name="phone" />
</div>
</div>
<DataConsentField
id="payment-consent"
checked={consent}
onCheckedChange={setConsent}
variant="payment"
/>
{error && <p className="text-sm text-destructive">{error}</p>}
</div>
<Card className="h-fit lg:col-span-2">
<CardHeader>
<CardTitle>Order summary</CardTitle>
<CardDescription>{tier.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<ul className="space-y-1 text-sm text-muted-foreground">
{tier.features.map((f) => (
<li key={f}>· {f}</li>
))}
</ul>
<div className="border-t pt-4">
<div className="flex justify-between text-sm">
<span>
{tier.name} × {quantity}
</span>
<span>${tier.priceUsd * quantity}</span>
</div>
<div className="mt-2 flex justify-between text-lg font-bold">
<span>Total</span>
<span className="text-[#30614c]">${total} USD</span>
</div>
</div>
<Button
type="submit"
className="w-full rounded-full bg-[#37a47a] text-[#ffffff] hover:bg-[#37a47a]/90"
disabled={status === "loading" || tier.soldOut}
>
{status === "loading"
? "Processing…"
: paymentMethod === "card"
? "Pay now"
: "Request invoice"}
</Button>
<p className="text-center text-xs text-muted-foreground">
Payments are processed securely. v1 records your order for follow-up.
</p>
</CardContent>
</Card>
</form>
);
}