Files
reui-blocks/blocks/product-card7.tsx
T

488 lines
14 KiB
TypeScript

"use client";
import { zodResolver } from "@hookform/resolvers/zod";
import { ClockFading, Heart, ShoppingBag } from "lucide-react";
import { Fragment } from "react";
import type { ControllerRenderProps, UseFormReturn } from "react-hook-form";
import { useForm } from "react-hook-form";
import { Controller } from "react-hook-form";
import z from "zod";
import { Price, PriceValue } from "@/components/shadcnblocks/price";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardTitle } from "@/components/ui/card";
import {
Drawer,
DrawerContent,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
import { Field, FieldLabel } from "@/components/ui/field";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
const STOCK_STATUS = {
IN_STOCK: "IN_STOCK",
OUT_OF_STOCK: "OUT_OF_STOCK",
LOW_STOCK: "LOW_STOCK",
} as const;
type StockStatusCode = keyof typeof STOCK_STATUS;
type option = {
id: string;
label: string;
value: string;
stockStatusCode: StockStatusCode;
};
type FormType = z.infer<typeof formSchema>;
type FieldName = keyof FormType;
interface ProductPrice {
regular: number;
sale?: number;
discountPercent?: string;
currency: string;
}
interface ProductFormProps {
sizeHinges?: {
name: FieldName;
label: string;
options: Array<option>;
};
}
interface SizeRadioGroupProps {
options: Array<option>;
field: ControllerRenderProps<FormType>;
form: UseFormReturn<FormType>;
onSubmit: (values: FormType) => void;
}
type SizeOptionProps = option;
interface Product {
name: string;
link: string;
image: {
src: string;
alt: string;
};
price: ProductPrice;
sizeHinges: {
name: FieldName;
label: string;
options: Array<option>;
};
}
const PRODUCT_CARD: Product = {
name: "سویشرت طرح‌دار ورزشی",
link: "#",
image: {
src: "/block-media/shadcnblocks/block/ecommerce/clothes/dynamic-sketches-sports-illustrated-sweatshirt.png",
alt: "سویشرت طرح‌دار ورزشی",
},
price: {
regular: 590000.0,
sale: 490000.0,
discountPercent: "-30%",
currency: "ریال",
},
sizeHinges: {
name: "size",
label: "انتخاب سایز",
options: [
{
id: "xs-1",
label: "xs",
value: "xs",
stockStatusCode: "OUT_OF_STOCK",
},
{
id: "s-1",
label: "s",
value: "s",
stockStatusCode: "OUT_OF_STOCK",
},
{
id: "m-1",
label: "m",
value: "m",
stockStatusCode: "IN_STOCK",
},
{
id: "l-1",
label: "l",
value: "l",
stockStatusCode: "LOW_STOCK",
},
{
id: "xl-1",
label: "xl",
value: "xl",
stockStatusCode: "LOW_STOCK",
},
],
},
};
interface ProductCard7Props {
className?: string;
}
const ProductCard7 = ({ className }: ProductCard7Props) => {
const { regular, sale, currency } = PRODUCT_CARD.price;
return (
<div className={cn("flex w-full justify-center", className)}>
<Card className="w-full max-w-64 border-none bg-background p-0 shadow-none">
<CardContent className="p-0">
<div className="group relative">
<a href={PRODUCT_CARD.link}>
<AspectRatio
ratio={1}
className="flex items-center justify-center overflow-hidden bg-muted/20"
>
<img
src={PRODUCT_CARD.image.src}
alt={PRODUCT_CARD.image.alt}
className="size-full object-contain object-center"
/>
</AspectRatio>
</a>
<div className="absolute inset-x-0 bottom-0 hidden border bg-background p-3 opacity-0 group-hover:opacity-100 lg:block">
<ProductDesktopForm sizeHinges={PRODUCT_CARD.sizeHinges} />
</div>
</div>
<div className="flex min-h-44 items-start justify-between gap-2 px-5 pb-8 pt-6 lg:px-7 lg:pb-8">
<a href={PRODUCT_CARD.link} className="flex flex-1 flex-col gap-1">
<CardTitle className="text-xl leading-none font-medium">
{PRODUCT_CARD.name}
</CardTitle>
<Price
onSale={sale != null}
className="flex-col items-start gap-1 text-xs"
>
<div className="flex items-center gap-1">
<PriceValue
currency={currency}
price={sale}
variant="sale"
className="font-semibold text-red-600"
/>
{PRODUCT_CARD.price.discountPercent && (
<Badge className="bg-red-600 px-1.5 py-px">
{PRODUCT_CARD.price.discountPercent}
</Badge>
)}
</div>
<PriceValue
price={regular}
currency={currency}
variant="regular"
/>
</Price>
</a>
<Button
variant="ghost"
size="icon"
className="hidden rounded-full lg:flex"
>
<Heart />
</Button>
<div className="lg:hidden">
<ProductMobileForm sizeHinges={PRODUCT_CARD.sizeHinges} />
</div>
</div>
</CardContent>
</Card>
</div>
);
};
const formSchema = z.object({
size: z.string().optional(),
});
const ProductDesktopForm = ({ sizeHinges }: ProductFormProps) => {
const form = useForm<FormType>({
resolver: zodResolver(formSchema),
defaultValues: sizeHinges ? { size: "" } : {},
});
function onSubmit(values: FormType) {
console.log(values);
}
return (
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col-reverse gap-11 lg:flex-col lg:gap-6"
>
{sizeHinges && (
<Controller
control={form.control}
name={`${sizeHinges.name}`}
render={({ field }) => (
<Field className="gap-3">
<FieldLabel asChild>
<p className="justify-center text-center text-xs leading-normal">
{sizeHinges.label}
</p>
</FieldLabel>
<SizeRadioGroup
form={form}
onSubmit={onSubmit}
field={field}
options={sizeHinges.options}
/>
</Field>
)}
/>
)}
{!sizeHinges && (
<Button variant="ghost" className="text-xs uppercase">
افزودن به سبد
</Button>
)}
</form>
);
};
const ProductMobileForm = ({ sizeHinges }: ProductFormProps) => {
const form = useForm<FormType>({
resolver: zodResolver(formSchema),
defaultValues: sizeHinges ? { size: "" } : {},
});
function onSubmit(values: FormType) {
console.log(values);
}
if (!sizeHinges) {
return (
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col-reverse gap-11 lg:flex-col lg:gap-6"
>
<Button variant="ghost" size="icon" className="rounded-full">
<ShoppingBag />
</Button>
</form>
);
}
return (
<Drawer>
<DrawerTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<ShoppingBag />
</Button>
</DrawerTrigger>
<DrawerContent aria-describedby={undefined}>
<DrawerHeader className="px-5">
<DrawerTitle className="text-sm">افزودن به سبد</DrawerTitle>
</DrawerHeader>
<div className="overflow-y-auto">
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col-reverse gap-11 lg:flex-col lg:gap-6"
>
{sizeHinges && (
<Controller
control={form.control}
name={`${sizeHinges.name}`}
render={({ field }) => (
<Field className="gap-3">
<FieldLabel asChild>
<p className="hidden justify-center text-center text-xs leading-normal lg:block">
{sizeHinges.label}
</p>
</FieldLabel>
<SizeRadioGroup
field={field}
options={sizeHinges.options}
form={form}
onSubmit={onSubmit}
/>
</Field>
)}
/>
)}
{!sizeHinges && (
<Button variant="ghost" className="text-xs uppercase">
افزودن به سبد
</Button>
)}
</form>
</div>
</DrawerContent>
</Drawer>
);
};
const SizeRadioGroup = ({
options,
field,
form,
onSubmit,
}: SizeRadioGroupProps) => {
if (!options) return;
const handleOnChange = async (value: string) => {
if (form.formState.isSubmitting) return;
if (value !== field.value && value) {
field.onChange(value);
const valid = await form.trigger(field.name);
if (valid) {
await form.handleSubmit(onSubmit)();
}
}
};
return (
<RadioGroup
{...field}
value={`${field.value}`}
onValueChange={handleOnChange}
className="flex w-full flex-col justify-center gap-0 lg:flex-row lg:flex-wrap lg:gap-3"
>
{options &&
options.map((item, index) => (
<Fragment key={`product-list-7-size-input-${index}`}>
<div className="hidden lg:contents">
<SizeOptionDesktop
stockStatusCode={item.stockStatusCode}
id={item.id}
label={item.label}
value={item.value}
/>
</div>
<div className="contents lg:hidden">
<SizeOptionMobile
stockStatusCode={item.stockStatusCode}
id={`${item.id}-mobile`}
label={item.label}
value={item.value}
/>
</div>
</Fragment>
))}
</RadioGroup>
);
};
const SizeOptionDesktop = ({
id,
label,
value,
stockStatusCode,
}: SizeOptionProps) => {
if (stockStatusCode === STOCK_STATUS.IN_STOCK) {
return (
<Field>
<FieldLabel
htmlFor={id}
className="flex h-8 min-w-8 cursor-pointer overflow-hidden rounded-full px-1.5 text-xs leading-none font-normal uppercase hover:ring has-checked:bg-primary has-checked:text-primary-foreground"
>
<RadioGroupItem
id={id}
className="sr-only"
value={value}
aria-label={`انتخاب ${label}`}
/>
<div className="m-auto">{label}</div>
</FieldLabel>
</Field>
);
}
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>
<Field>
<FieldLabel
htmlFor={id}
className="group/size flex h-8 min-w-8 cursor-pointer overflow-hidden rounded-full px-1.5 text-xs leading-none font-normal uppercase not-has-disabled:hover:ring has-checked:bg-primary has-checked:text-primary-foreground has-disabled:bg-muted has-disabled:text-muted-foreground"
>
<RadioGroupItem
id={id}
className="sr-only"
value={value}
aria-label={`انتخاب ${label}`}
disabled={stockStatusCode === STOCK_STATUS.OUT_OF_STOCK}
/>
<div className="m-auto flex items-center justify-center gap-1.5">
{stockStatusCode === STOCK_STATUS.LOW_STOCK && (
<ClockFading className="size-4 stroke-muted-foreground group-has-checked/size:stroke-primary-foreground" />
)}
{label}
</div>
</FieldLabel>
</Field>
</div>
</TooltipTrigger>
<TooltipContent
side="bottom"
className="bg-background px-5 shadow [&>span:nth-child(2)>svg]:bg-background [&>span:nth-child(2)>svg]:fill-background"
>
<span className="text-center text-xs text-muted-foreground">
{stockStatusCode === STOCK_STATUS.LOW_STOCK
? "تنها چند عدد مانده"
: "ناموجود"}
</span>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
};
const SizeOptionMobile = ({
id,
label,
value,
stockStatusCode,
}: SizeOptionProps) => {
return (
<Field>
<FieldLabel
htmlFor={id}
className="px-5 pt-5 text-sm leading-normal font-normal uppercase not-has-disabled:active:bg-muted has-disabled:text-muted-foreground"
>
<RadioGroupItem
id={id}
className="sr-only"
value={value}
aria-label={`انتخاب ${label}`}
disabled={stockStatusCode === STOCK_STATUS.OUT_OF_STOCK}
/>
<div className="flex w-full items-center justify-between gap-2 border-b pb-5">
{label}
<span className="text-center text-xs text-muted-foreground normal-case">
{stockStatusCode === STOCK_STATUS.LOW_STOCK && "تنها چند عدد مانده!"}
</span>
</div>
</FieldLabel>
</Field>
);
};
export { ProductCard7 };