Components
Starting preview...
Enhanced shadcn/ui dialog
npx shadcn@latest add https://21st.dev/r/originui/dialog"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { CreditCard, Store } from "lucide-react";
import { useEffect, useId, useRef, useState } from "react";
import { usePaymentInputs } from "react-payment-inputs";
import images, { type CardImages } from "react-payment-inputs/images";
function Component() {
const id = useId();
const { meta, getCardNumberProps, getExpiryDateProps, getCVCProps, getCardImageProps } =
usePaymentInputs();
const couponInputRef = useRef<HTMLInputElement>(null);
const [showCouponInput, setShowCouponInput] = useState(false);
const [couponCode, setCouponCode] = useState("");
useEffect(() => {
if (showCouponInput && couponInputRef.current) {
couponInputRef.current.focus();
}
}, [showCouponInput]);
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Checkout</Button>
</DialogTrigger>
<DialogContent>
<div className="mb-2 flex flex-col gap-2">
<div
className="flex size-11 shrink-0 items-center justify-center rounded-full border border-border"
aria-hidden="true"
>
<Store className="opacity-80" size={16} strokeWidth={2} />
</div>
<DialogHeader>
<DialogTitle className="text-left">Confirm and pay</DialogTitle>
<DialogDescription className="text-left">
Pay securely and cancel any time.
</DialogDescription>
</DialogHeader>
</div>
<form className="space-y-5">
<div className="space-y-4">
<RadioGroup className="grid-cols-2" defaultValue="yearly">
<label className="relative flex cursor-pointer flex-col gap-1 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 outline-offset-2 transition-colors has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-accent has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-ring/70">
<RadioGroupItem
id="radio-monthly"
value="monthly"
className="sr-only after:absolute after:inset-0"
/>
<p className="text-sm font-medium text-foreground">Monthly</p>
<p className="text-sm text-muted-foreground">$32/month</p>
</label>
<label className="relative flex cursor-pointer flex-col gap-1 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 outline-offset-2 transition-colors has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-accent has-[:focus-visible]:outline has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-ring/70">
<RadioGroupItem
id="radio-yearly"
value="yearly"
className="sr-only after:absolute after:inset-0"
/>
<div className="inline-flex items-start justify-between gap-2">
<p className="text-sm font-medium text-foreground">Yearly</p>
<Badge>Popular</Badge>
</div>
<p className="text-sm text-muted-foreground">$320/month</p>
</label>
</RadioGroup>
<div className="space-y-2">
<Label htmlFor={`name-${id}`}>Name on card</Label>
<Input id={`name-${id}`} type="text" required />
</div>
<div className="space-y-2">
<legend className="text-sm font-medium text-foreground">Card Details</legend>
<div className="rounded-lg shadow-sm shadow-black/5">
<div className="relative focus-within:z-10">
<Input
className="peer rounded-b-none pe-9 shadow-none [direction:inherit]"
{...getCardNumberProps()}
/>
<div className="pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 text-muted-foreground/80 peer-disabled:opacity-50">
{meta.cardType ? (
<svg
className="overflow-hidden rounded-sm"
{...getCardImageProps({ images: images as unknown as CardImages })}
width={20}
/>
) : (
<CreditCard size={16} strokeWidth={2} aria-hidden="true" />
)}
</div>
</div>
<div className="-mt-px flex">
<div className="min-w-0 flex-1 focus-within:z-10">
<Input
className="rounded-e-none rounded-t-none shadow-none [direction:inherit]"
{...getExpiryDateProps()}
/>
</div>
<div className="-ms-px min-w-0 flex-1 focus-within:z-10">
<Input
className="rounded-s-none rounded-t-none shadow-none [direction:inherit]"
{...getCVCProps()}
/>
</div>
</div>
</div>
</div>
{!showCouponInput ? (
<button
type="button"
onClick={() => setShowCouponInput(true)}
className="text-sm underline hover:no-underline"
>
+ Add coupon
</button>
) : (
<div className="space-y-2">
<Label htmlFor={`coupon-${id}`}>Coupon code</Label>
<Input
id={`coupon-${id}`}
ref={couponInputRef}
placeholder="Enter your code"
value={couponCode}
onChange={(e) => setCouponCode(e.target.value)}
/>
</div>
)}
</div>
<Button type="button" className="w-full">
Subscribe
</Button>
</form>
<p className="text-center text-xs text-muted-foreground">
Payments are non-refundable. Cancel anytime.
</p>
</DialogContent>
</Dialog>
);
}
export { Component };