490 lines
16 KiB
TypeScript
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 };
|