Files
app/checkout-form/page.tsx
"use client"

import * as React from "react"
import { ArrowLeft, Check, CreditCard, Lock, Zap } from "lucide-react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"

const plan = {
  name: "Pro Plan",
  price: 29,
  description: "Everything you need to grow and scale your business.",
  features: [
    "Unlimited projects",
    "100 GB storage",
    "Advanced analytics",
    "Priority support",
    "API access",
    "Custom integrations",
    "Team collaboration",
  ],
}

const COUNTRIES = [
  { code: "US", name: "United States" },
  { code: "GB", name: "United Kingdom" },
  { code: "CA", name: "Canada" },
  { code: "AU", name: "Australia" },
  { code: "DE", name: "Germany" },
  { code: "FR", name: "France" },
  { code: "ES", name: "Spain" },
  { code: "IT", name: "Italy" },
  { code: "NL", name: "Netherlands" },
  { code: "MA", name: "Morocco" },
]

type PaymentStatus = "idle" | "processing" | "success" | "error"

function formatCardNumber(val: string) {
  return val
    .replace(/\D/g, "")
    .slice(0, 16)
    .replace(/(\d{4})(?=\d)/g, "$1 ")
}

function formatExpiry(val: string) {
  const digits = val.replace(/\D/g, "").slice(0, 4)
  return digits.length > 2 ? digits.slice(0, 2) + "/" + digits.slice(2) : digits
}

export default function CheckoutForm() {
  const [status, setStatus] = React.useState<PaymentStatus>("idle")
  const [form, setForm] = React.useState({
    name: "",
    email: "",
    cardNumber: "",
    expiry: "",
    cvc: "",
    country: "",
  })

  function handleChange(
    e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>
  ) {
    const { name, value } = e.target
    if (name === "cardNumber") {
      setForm((f) => ({ ...f, cardNumber: formatCardNumber(value) }))
    } else if (name === "expiry") {
      setForm((f) => ({ ...f, expiry: formatExpiry(value) }))
    } else if (name === "cvc") {
      setForm((f) => ({ ...f, cvc: value.replace(/\D/g, "").slice(0, 4) }))
    } else {
      setForm((f) => ({ ...f, [name]: value }))
    }
  }

  function handleSubmit(e: React.FormEvent) {
    e.preventDefault()
    setStatus("processing")
    setTimeout(() => {
      setStatus("success")
      setTimeout(() => setStatus("idle"), 3000)
    }, 1800)
  }

  const buttonLabel: Record<PaymentStatus, string> = {
    idle: `Pay $${plan.price}/mo`,
    processing: "Processing...",
    success: "Payment succeeded 🎉",
    error: "Try again",
  }

  return (
    <div className="flex min-h-screen flex-col lg:flex-row">
      <div className="bg-background w-full overflow-y-auto p-6 sm:p-10 lg:w-[45%]">
        <div className="mx-auto max-w-md">
          <a
            href="#"
            className="text-muted-foreground hover:text-foreground mb-8 inline-flex items-center gap-2 text-sm transition-colors"
          >
            <ArrowLeft className="h-4 w-4" />
            Back to plans
          </a>

          <div className="mb-8">
            <h1 className="mb-1 text-2xl font-semibold tracking-tight">
              Subscribe to {plan.name}
            </h1>
            <p className="text-muted-foreground text-sm">{plan.description}</p>
          </div>

          <form onSubmit={handleSubmit} className="space-y-4">
            <div className="space-y-1.5">
              <Label htmlFor="name">Cardholder name</Label>
              <Input
                id="name"
                name="name"
                placeholder="Jane Smith"
                value={form.name}
                onChange={handleChange}
                required
                autoComplete="cc-name"
              />
            </div>

            <div className="space-y-1.5">
              <Label htmlFor="email">Email</Label>
              <Input
                id="email"
                name="email"
                type="email"
                placeholder="jane@example.com"
                value={form.email}
                onChange={handleChange}
                required
                autoComplete="email"
              />
            </div>

            <div className="space-y-1.5">
              <Label htmlFor="cardNumber">Card number</Label>
              <div className="relative">
                <Input
                  id="cardNumber"
                  name="cardNumber"
                  placeholder="1234 5678 9012 3456"
                  value={form.cardNumber}
                  onChange={handleChange}
                  required
                  autoComplete="cc-number"
                  inputMode="numeric"
                  className="pr-10"
                />
                <CreditCard className="text-muted-foreground pointer-events-none absolute top-1/2 right-3 h-4 w-4 -translate-y-1/2" />
              </div>
            </div>

            <div className="grid grid-cols-2 gap-4">
              <div className="space-y-1.5">
                <Label htmlFor="expiry">Expiry</Label>
                <Input
                  id="expiry"
                  name="expiry"
                  placeholder="MM/YY"
                  value={form.expiry}
                  onChange={handleChange}
                  required
                  autoComplete="cc-exp"
                  inputMode="numeric"
                />
              </div>
              <div className="space-y-1.5">
                <Label htmlFor="cvc">CVC</Label>
                <Input
                  id="cvc"
                  name="cvc"
                  placeholder="123"
                  value={form.cvc}
                  onChange={handleChange}
                  required
                  autoComplete="cc-csc"
                  inputMode="numeric"
                />
              </div>
            </div>

            <div className="space-y-1.5">
              <Label htmlFor="country">Country</Label>
              <select
                id="country"
                name="country"
                value={form.country}
                onChange={handleChange}
                required
                className={cn(
                  "border-input bg-background ring-offset-background focus-visible:ring-ring flex h-10 w-full rounded-md border px-3 py-2 text-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none",
                  !form.country && "text-muted-foreground"
                )}
              >
                <option value="" disabled>
                  Select country
                </option>
                {COUNTRIES.map((c) => (
                  <option key={c.code} value={c.code}>
                    {c.name}
                  </option>
                ))}
              </select>
            </div>

            <Button
              type="submit"
              size="lg"
              className="w-full"
              disabled={status === "processing" || status === "success"}
            >
              {buttonLabel[status]}
            </Button>
          </form>

          <p className="text-muted-foreground mt-4 text-xs">
            By subscribing, you agree to our Terms of Service and authorize us
            to charge your card for future payments in accordance with our
            terms.
          </p>
        </div>
      </div>

      <div className="bg-muted hidden lg:block lg:w-[55%]">
        <div className="sticky top-0 h-screen overflow-y-auto p-12">
          <div className="mx-auto max-w-xl">
            <div className="mb-8 flex items-center gap-3">
              <div className="bg-primary/10 flex h-10 w-10 items-center justify-center rounded-full">
                <Zap className="text-primary h-5 w-5" />
              </div>
              <h3 className="text-xl font-medium">{plan.name}</h3>
            </div>

            <div className="mb-12">
              <div className="mb-2 flex items-baseline gap-2">
                <span className="text-5xl font-semibold">${plan.price}</span>
                <span className="text-muted-foreground text-lg">per month</span>
              </div>
              <p className="text-muted-foreground text-sm">
                Billed monthly. Cancel anytime.
              </p>
            </div>

            <ul className="mb-10 space-y-3">
              {plan.features.map((feature) => (
                <li key={feature} className="flex items-center gap-3">
                  <div className="bg-primary/10 flex h-5 w-5 shrink-0 items-center justify-center rounded-full">
                    <Check className="text-primary h-3 w-3" />
                  </div>
                  <span className="text-sm">{feature}</span>
                </li>
              ))}
            </ul>

            <div className="text-muted-foreground flex items-center gap-2 text-sm">
              <Lock className="h-4 w-4 shrink-0" />
              <span>Secure checkout. Your payment info is encrypted.</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  )
}
Split-panel checkout form with card input and plan summary
checkout-form-01