"use client";
import { useState, useEffect } from "react";
import { CreditCard, Check, Lock } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
type CardType = "visa" | "mastercard" | "amex" | "discover" | "unknown";
const detectCardType = (number: string): CardType => {
const cleaned = number.replace(/\s/g, "");
if (/^4/.test(cleaned)) return "visa";
if (/^5[1-5]/.test(cleaned) || /^2[2-7]/.test(cleaned)) return "mastercard";
if (/^3[47]/.test(cleaned)) return "amex";
if (/^6(?:011|5)/.test(cleaned)) return "discover";
return "unknown";
};
const formatCardNumber = (value: string, cardType: CardType): string => {
const cleaned = value.replace(/\D/g, "");
if (cardType === "amex") {
const match = cleaned.match(/^(\d{0,4})(\d{0,6})(\d{0,5})$/);
if (match) {
return [match[1], match[2], match[3]].filter(Boolean).join(" ");
}
}
const match = cleaned.match(/^(\d{0,4})(\d{0,4})(\d{0,4})(\d{0,4})$/);
if (match) {
return [match[1], match[2], match[3], match[4]].filter(Boolean).join(" ");
}
return cleaned;
};
const formatExpiry = (value: string): string => {
const cleaned = value.replace(/\D/g, "");
if (cleaned.length >= 2) {
return `${cleaned.slice(0, 2)}/${cleaned.slice(2, 4)}`;
}
return cleaned;
};
const CardIcon = ({ type }: { type: CardType }) => {
const baseClass = "h-6 w-8 rounded border flex items-center justify-center text-xs font-bold";
if (type === "unknown") {
return (
<div className={`${baseClass} border-border bg-muted text-muted-foreground`}>
<CreditCard className="h-4 w-4" />
</div>
);
}
const labels: Record<Exclude<CardType, "unknown">, string> = {
visa: "VISA",
mastercard: "MC",
amex: "AMEX",
discover: "DISC",
};
return (
<div className={`${baseClass} border-border bg-muted/50 text-foreground`}>
{labels[type]}
</div>
);
};
export const title = "React Dialog Block Payment Method";
export default function DialogPaymentMethod() {
const [open, setOpen] = useState(false);
const [saved, setSaved] = useState(false);
const [saveCard, setSaveCard] = useState(false);
const [cardNumber, setCardNumber] = useState("");
const [cardName, setCardName] = useState("");
const [expiry, setExpiry] = useState("");
const [cvv, setCvv] = useState("");
const [cardType, setCardType] = useState<CardType>("unknown");
useEffect(() => {
setCardType(detectCardType(cardNumber));
}, [cardNumber]);
const handleCardNumberChange = (value: string) => {
const type = detectCardType(value);
const maxLength = type === "amex" ? 17 : 19; // Including spaces
const formatted = formatCardNumber(value, type);
if (formatted.length <= maxLength) {
setCardNumber(formatted);
}
};
const handleExpiryChange = (value: string) => {
const formatted = formatExpiry(value);
if (formatted.length <= 5) {
setExpiry(formatted);
}
};
const handleCvvChange = (value: string) => {
const cleaned = value.replace(/\D/g, "");
const maxLength = cardType === "amex" ? 4 : 3;
if (cleaned.length <= maxLength) {
setCvv(cleaned);
}
};
const handleSave = () => {
console.log({ cardNumber, cardName, expiry, cvv, saveCard });
setSaved(true);
};
const handleClose = () => {
setOpen(false);
setTimeout(() => {
setSaved(false);
setCardNumber("");
setCardName("");
setExpiry("");
setCvv("");
setSaveCard(false);
setCardType("unknown");
}, 200);
};
const isValidExpiry = () => {
if (expiry.length !== 5) return false;
const [month, year] = expiry.split("/");
const m = parseInt(month, 10);
const y = parseInt(`20${year}`, 10);
if (m < 1 || m > 12) return false;
const now = new Date();
const currentYear = now.getFullYear();
const currentMonth = now.getMonth() + 1;
if (y < currentYear || (y === currentYear && m < currentMonth)) return false;
return true;
};
const isValid =
cardNumber.replace(/\s/g, "").length >= (cardType === "amex" ? 15 : 16) &&
cardName.trim().length > 0 &&
isValidExpiry() &&
cvv.length >= (cardType === "amex" ? 4 : 3);
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<CreditCard className="mr-2 h-4 w-4" />
Add Payment Method
</Button>
</DialogTrigger>
</div>
<DialogContent className="sm:max-w-md">
{saved ? (
<>
<DialogHeader className="text-center">
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-primary/10 mb-4">
<Check className="h-6 w-6 text-primary" />
</div>
<DialogTitle>Card Added</DialogTitle>
<DialogDescription>
Your payment method ending in {cardNumber.slice(-4)} has been saved
{saveCard && " and will be available for future purchases"}.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button onClick={handleClose} className="w-full">
Done
</Button>
</DialogFooter>
</>
) : (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<CreditCard className="h-5 w-5" />
Add Payment Method
</DialogTitle>
<DialogDescription>
Enter your card details securely.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="cardNumber">Card Number</Label>
<div className="relative">
<Input
id="cardNumber"
placeholder="1234 5678 9012 3456"
value={cardNumber}
onChange={(e) => handleCardNumberChange(e.target.value)}
className="pr-16"
autoComplete="cc-number"
/>
<div className="absolute right-3 top-1/2 -translate-y-1/2">
<CardIcon type={cardType} />
</div>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="cardName">Name on Card</Label>
<Input
id="cardName"
placeholder="John Smith"
value={cardName}
onChange={(e) => setCardName(e.target.value)}
autoComplete="cc-name"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="expiry">Expiry Date</Label>
<Input
id="expiry"
placeholder="MM/YY"
value={expiry}
onChange={(e) => handleExpiryChange(e.target.value)}
autoComplete="cc-exp"
/>
</div>
<div className="grid gap-2">
<Label htmlFor="cvv">
CVV
<span className="ml-1 text-xs text-muted-foreground">
({cardType === "amex" ? "4 digits" : "3 digits"})
</span>
</Label>
<Input
id="cvv"
type="password"
placeholder={cardType === "amex" ? "••••" : "•••"}
value={cvv}
onChange={(e) => handleCvvChange(e.target.value)}
autoComplete="cc-csc"
/>
</div>
</div>
<div className="flex items-center space-x-2 pt-2">
<Checkbox
id="saveCard"
checked={saveCard}
onCheckedChange={(checked) => setSaveCard(checked as boolean)}
/>
<Label
htmlFor="saveCard"
className="text-sm font-normal cursor-pointer"
>
Save this card for future purchases
</Label>
</div>
<div className="flex items-center gap-2 text-xs text-muted-foreground pt-2">
<Lock className="h-3 w-3" />
<span>Your payment info is encrypted and secure</span>
</div>
</div>
<DialogFooter className="gap-2 sm:gap-0">
<Button variant="outline" onClick={handleClose}>
Cancel
</Button>
<Button onClick={handleSave} disabled={!isValid}>
Add Card
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
);
}