Files

490 lines
16 KiB
TypeScript

"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { ArrowLeft, Lock } from "lucide-react";
import React from "react";
import {
Controller,
FormProvider,
useForm,
useFormContext,
} from "react-hook-form";
import z from "zod";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";
interface OrderItem {
id: string;
name: string;
price: number;
}
interface OrderSummary {
companyName: string;
items: OrderItem[];
currency: string;
}
interface Checkout5Props {
order: OrderSummary;
className?: string;
}
const formatPrice = (price: number, currency = "IRR") =>
new Intl.NumberFormat("fa-IR", {
style: "currency",
currency,
}).format(price);
const checkoutFormSchema = z.object({
email: z.string().email("ایمیل نامعتبر است"),
cardNumber: z.string().min(1, "شماره کارت الزامی است"),
expiryDate: z
.string()
.regex(/^(0[1-9]|1[0-2])\/\d{2}$/, "فرمت نامعتبر (MM/YY)")
.refine((value) => {
const [mm, yy] = value.split("/").map(Number);
const now = new Date();
const currentMonth = now.getMonth() + 1;
const currentYear = now.getFullYear() % 100;
if (yy < currentYear) return false;
if (yy === currentYear && mm < currentMonth) return false;
return true;
}, "کارت منقضی شده است"),
cvc: z.string().min(3, "کد امنیتی الزامی است"),
cardholderName: z.string().min(1, "نام روی کارت الزامی است"),
country: z.string().min(1, "انتخاب کشور الزامی است"),
promoCode: z.string().optional(),
});
type CheckoutFormType = z.infer<typeof checkoutFormSchema>;
const COUNTRIES = [
{ value: "AU", label: "استرالیا" },
{ value: "IR", label: "ایران" },
{ value: "TR", label: "ترکیه" },
{ value: "IQ", label: "عراق" },
{ value: "PK", label: "پاکستان" },
{ value: "AFG", label: "افغانستان" },
];
const DEFAULT_ORDER: OrderSummary = {
companyName: "ریو آی",
items: [
{
id: "pro-subscription",
name: "اشتراک حرفه‌ای (سالانه)",
price: 199.0,
},
],
currency: "IRR",
};
const Checkout5 = ({ order = DEFAULT_ORDER, className }: Checkout5Props) => {
const form = useForm<CheckoutFormType>({
resolver: zodResolver(checkoutFormSchema),
defaultValues: {
email: "",
cardNumber: "",
expiryDate: "ماه/سال",
cvc: "کد امنیتی",
cardholderName: "",
country: "IR",
promoCode: "",
},
});
const onSubmit = (data: CheckoutFormType) => {
console.log("Form submitted:", data);
};
const subtotal = order.items.reduce((sum, item) => sum + item.price, 0);
const total = subtotal;
return (
<section
dir="rtl"
lang="fa"
className={cn("min-h-screen bg-background", className)}
>
<FormProvider {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<div className="grid min-h-screen lg:grid-cols-2">
<div className="border-e border-border bg-muted/20 p-8 pt-32 lg:p-12 lg:pt-40 xl:p-16 xl:pt-48">
<div className="mx-auto w-full max-w-md">
<div className="mb-8 flex items-center gap-3">
<button
type="button"
className="flex size-8 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
aria-label="بازگشت به سبد"
>
<ArrowLeft className="size-4 rtl:rotate-180" />
</button>
<img
src="/block-media/shadcnblocks/block/logos/ShadcnBlockscom-icon.svg"
alt={order.companyName}
className="size-8"
/>
</div>
{/* Company and Total */}
<div className="mb-8">
<p className="text-muted-foreground">{order.companyName}</p>
<p className="text-xl font-semibold tracking-tight tabular-nums">
{formatPrice(total, order.currency)}
</p>
</div>
{/* Order Items */}
<div className="space-y-4">
{order.items.map((item) => (
<div
key={item.id}
className="flex items-center justify-between"
>
<span className="text-sm text-foreground">
{item.name}
</span>
<span className="text-sm tabular-nums text-foreground">
{formatPrice(item.price, order.currency)}
</span>
</div>
))}
</div>
<Separator className="my-4" />
{/* Subtotal */}
<div className="flex items-center justify-between">
<span className="text-sm text-foreground">جمع سفارش</span>
<span className="text-sm tabular-nums text-foreground">
{formatPrice(subtotal, order.currency)}
</span>
</div>
{/* Promo Code Button */}
<div className="mt-4">
<PromoCodeField />
</div>
<Separator className="my-4" />
{/* Total */}
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-foreground">
مبلغ قابل پرداخت
</span>
<span className="text-sm font-medium tabular-nums text-foreground">
{formatPrice(total, order.currency)}
</span>
</div>
</div>
</div>
<div className="p-8 pt-32 lg:p-12 lg:pt-40 xl:p-16 xl:pt-48">
<div className="mx-auto w-full max-w-md">
<div className="my-6 flex items-center gap-4">
<Separator className="flex-1" />
<span className="text-sm text-muted-foreground">
با کارت بانکی پرداخت کنید
</span>
<Separator className="flex-1" />
</div>
<div className="space-y-6">
<EmailField />
<div>
<h2 className="mb-4 text-sm font-medium">جزئیات کارت</h2>
<PaymentFields />
</div>
<CountryField />
<Button
type="submit"
size="lg"
className="h-12 w-full bg-blue-600 text-base text-white hover:bg-blue-700"
>
پرداخت و ثبت سفارش
</Button>
<p className="text-center text-xs text-muted-foreground">
با ثبت سفارش، شرایط استفاده و سیاست حفظ حریم خصوصی را می‌پذیرید. اطلاعات پرداخت شما با استانداردهای امنیتی محافظت و رمزنگاری می‌شود.
</p>
<div className="flex items-center justify-center gap-4 pt-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Lock className="size-3" />
پرداخت امن اشتراک
</span>
<Separator orientation="vertical" className="h-4" />
<a
href="#"
className="hover:text-foreground hover:underline"
>
شرایط استفاده
</a>
<a
href="#"
className="hover:text-foreground hover:underline"
>
حریم خصوصی
</a>
</div>
</div>
</div>
</div>
</div>
</form>
</FormProvider>
</section>
);
};
const PromoCodeField = () => {
const form = useFormContext<CheckoutFormType>();
const [showInput, setShowInput] = React.useState(false);
if (!showInput) {
return (
<Button
type="button"
variant="outline"
size="sm"
className="h-9 text-sm"
onClick={() => setShowInput(true)}
>
افزودن کد تخفیف
</Button>
);
}
return (
<Controller
name="promoCode"
control={form.control}
render={({ field }) => (
<div className="flex gap-2">
<Input
{...field}
dir="rtl"
placeholder="کد تخفیف"
className="h-9 flex-1 text-sm"
/>
<Button type="button" variant="outline" size="sm" className="h-9">
اعمال
</Button>
</div>
)}
/>
);
};
const EmailField = () => {
const form = useFormContext<CheckoutFormType>();
return (
<Controller
name="email"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel className="text-sm font-medium" htmlFor="checkout-email">
ایمیل
</FieldLabel>
<Input
{...field}
id="checkout-email"
type="email"
dir="rtl"
placeholder="example@email.com"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
);
};
const PaymentFields = () => {
const form = useFormContext<CheckoutFormType>();
return (
<div className="space-y-4">
{/* Card Number */}
<Controller
name="cardNumber"
control={form.control}
render={({ field, fieldState }) => (
<div className="space-y-2">
<div className="flex items-center justify-between">
<FieldLabel
className="text-sm text-muted-foreground"
htmlFor="checkout-card-number"
>
شماره کارت
</FieldLabel>
<div className="flex items-center gap-1">
<img
src="/block-media/shadcnblocks/block/logos/visa-icon.svg"
alt="Visa"
className="h-5 w-auto"
/>
<img
src="/block-media/shadcnblocks/block/logos/mastercard-logo.svg"
alt="Mastercard"
className="h-5 w-auto"
/>
<img
src="/block-media/shadcnblocks/block/logos/american-express-icon.svg"
alt="کارت ویژه"
className="h-5 w-auto"
/>
</div>
</div>
<Field data-invalid={fieldState.invalid}>
<Input
{...field}
id="checkout-card-number"
dir="rtl"
placeholder="1234 1234 1234 1234"
aria-invalid={fieldState.invalid}
className="rounded-b-none border-b-0"
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
</div>
)}
/>
{/* Expiry and CVC Row */}
<div className="-mt-4 grid grid-cols-2">
<Controller
name="expiryDate"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
{...field}
onChange={(e) => {
let val = e.target.value;
val = val.replace(/[^0-9/]/g, "");
const prev = field.value ?? "";
const isDeleting = val.length < prev.length;
if (!isDeleting) {
if (val.length === 2 && !val.includes("/")) {
val = val + "/";
}
}
if (val.length > 5) {
val = val.slice(0, 5);
}
field.onChange(val);
}}
placeholder="MM/YY"
dir="rtl"
className="rounded-none rounded-bl-md border-r-0"
aria-label="تاریخ انقضا"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
<Controller
name="cvc"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<Input
{...field}
dir="rtl"
placeholder="CVC"
aria-label="کد امنیتی (CVC)"
className="rounded-none rounded-br-md"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</div>
{/* Cardholder Name */}
<Controller
name="cardholderName"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel
className="text-sm font-medium"
htmlFor="checkout-cardholder-name"
>
نام روی کارت
</FieldLabel>
<Input
{...field}
id="checkout-cardholder-name"
placeholder="نام و نام خانوادگی"
aria-invalid={fieldState.invalid}
/>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
</div>
);
};
const CountryField = () => {
const form = useFormContext<CheckoutFormType>();
return (
<Controller
name="country"
control={form.control}
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel
className="text-sm font-medium"
htmlFor="checkout-country"
>
کشور صورتحساب
</FieldLabel>
<Select dir="rtl" value={field.value} onValueChange={field.onChange}>
<SelectTrigger id="checkout-country" className="w-full ">
<SelectValue placeholder="کشور را انتخاب کنید" />
</SelectTrigger>
<SelectContent>
{COUNTRIES.map((country) => (
<SelectItem key={country.value} value={country.value}>
{country.label}
</SelectItem>
))}
</SelectContent>
</Select>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>
);
};
export { Checkout5 };