category detail

This commit is contained in:
hamid zarghami
2026-07-21 09:48:39 +03:30
parent 6ae9a6a17f
commit fe1e545aaf
10 changed files with 254 additions and 13 deletions
@@ -0,0 +1,87 @@
"use client";
import Input from "@/app/components/Input";
import { cn } from "@/app/lib/cn";
import { AddSquare, MinusSquare } from "iconsax-reactjs";
import { useState, type FC } from "react";
import { FILTER_SECTIONS, type FilterSection } from "../constants";
const CategoryFilters: FC = () => {
const [openSections, setOpenSections] = useState<Record<string, boolean>>(() => Object.fromEntries(FILTER_SECTIONS.map((section) => [section.id, Boolean(section.defaultOpen)])));
const [selected, setSelected] = useState<string[]>([]);
const toggleSection = (id: string) => {
setOpenSections((prev) => ({ ...prev, [id]: !prev[id] }));
};
const toggleOption = (option: string) => {
setSelected((prev) => (prev.includes(option) ? prev.filter((item) => item !== option) : [...prev, option]));
};
return (
<div className="w-full shrink-0 space-y-3 lg:w-70 xl:w-75">
<Input variant="search" placeholder="جستجو..." className="rounded-xl border-[#E9EFF4]" />
<aside className="rounded-2xl bg-white p-4 shadow-[0_0_6px_rgba(50,103,137,0.18)] sm:rounded-3xl sm:p-5">
<h2 className="mb-4 text-center text-lg font-bold text-[#0A1B2C]">فیلترها</h2>
<div className="flex flex-col gap-3">
{FILTER_SECTIONS.map((section) => (
<FilterAccordion
key={section.id}
section={section}
isOpen={Boolean(openSections[section.id])}
selected={selected}
onToggle={() => toggleSection(section.id)}
onToggleOption={toggleOption}
/>
))}
</div>
</aside>
</div>
);
};
type FilterAccordionProps = {
section: FilterSection;
isOpen: boolean;
selected: string[];
onToggle: () => void;
onToggleOption: (option: string) => void;
};
const FilterAccordion: FC<FilterAccordionProps> = ({ section, isOpen, selected, onToggle, onToggleOption }) => {
const Icon = isOpen ? MinusSquare : AddSquare;
return (
<div className="rounded-xl bg-[rgba(233,238,242,0.35)] p-3">
<button type="button" className="flex w-full items-center justify-between gap-2" onClick={onToggle} aria-expanded={isOpen}>
<span className="text-sm text-[#0A1B2C] sm:text-base">{section.title}</span>
<Icon size={16} color="currentColor" className="shrink-0 text-primary" />
</button>
{isOpen && section.options && (
<ul className="mt-3 flex flex-col gap-2.5">
{section.options.map((option) => {
const checked = selected.includes(option);
return (
<li key={option}>
<button type="button" className="flex w-full items-center gap-2" onClick={() => onToggleOption(option)}>
<span
role="checkbox"
aria-checked={checked}
className={cn("size-4 shrink-0 rounded border-2 transition-colors", checked ? "border-primary bg-primary" : "border-[rgba(50,103,137,0.29)] bg-transparent")}
/>
<span className="text-right text-sm text-[#53606B]">{option}</span>
</button>
</li>
);
})}
</ul>
)}
</div>
);
};
export default CategoryFilters;
+47
View File
@@ -0,0 +1,47 @@
import heroImage from "@/assets/images/category/hero.jpg";
import { ArrowLeft2 } from "iconsax-reactjs";
import Image from "next/image";
import Link from "next/link";
import { type FC } from "react";
import { CATEGORY_DESCRIPTION, CATEGORY_TITLE } from "../constants";
const CategoryHero: FC = () => {
return (
<>
<section
className="relative overflow-x-clip px-4 sm:px-8 lg:px-30 py-6 sm:py-8 lg:py-10"
style={{
backgroundImage: "linear-gradient(108.5deg, rgba(33, 88, 140, 0.1) 89.65%, rgba(237, 243, 248, 0.3) 100%)",
}}
>
<div className="flex flex-col lg:flex-row lg:items-center gap-6 lg:gap-10">
<div className="flex-1 space-y-3 sm:space-y-4">
<h1 className="text-[#0A1B2C] text-2xl sm:text-3xl lg:text-[32px] font-bold">{CATEGORY_TITLE}</h1>
<p className="text-sm text-[#0A1B2C]/80 leading-7 max-w-140">{CATEGORY_DESCRIPTION}</p>
</div>
<div className="relative z-10 mx-auto shrink-0 lg:mx-0 lg:-mb-28">
<Image
src={heroImage}
alt={CATEGORY_TITLE}
width={360}
height={360}
priority
className="aspect-square w-55 sm:w-70 lg:w-[320px] rounded-2xl sm:rounded-3xl object-cover shadow-[0px_-4px_12px_0px_rgba(0,0,0,0.12)]"
/>
</div>
</div>
</section>
<nav aria-label="مسیر صفحه" className="relative z-0 flex items-center justify-start gap-2 bg-secondary px-4 sm:px-8 lg:px-30 py-3 text-sm text-[#3F4D5A]">
<Link href="/" className="transition-colors hover:text-primary">
صفحه اصلی
</Link>
<ArrowLeft2 size={14} color="currentColor" className="shrink-0" />
<span className="font-bold">{CATEGORY_TITLE}</span>
</nav>
</>
);
};
export default CategoryHero;
@@ -0,0 +1,16 @@
import GridWrapper from "@/app/components/GridWrapper";
import ProductCard from "@/app/home/components/ProductCard";
import { type FC } from "react";
import { CATEGORY_PRODUCTS } from "../constants";
const CategoryProducts: FC = () => {
return (
<GridWrapper desktop={4} mobile={2} gapDesktop={24} gapMobile={12}>
{CATEGORY_PRODUCTS.map((title, index) => (
<ProductCard key={`${title}-${index}`} title={title} />
))}
</GridWrapper>
);
};
export default CategoryProducts;
@@ -0,0 +1,22 @@
import { Sort } from "iconsax-reactjs";
import { type FC } from "react";
import { SORT_OPTIONS } from "../constants";
const CategoryToolbar: FC = () => {
return (
<div className="flex items-center justify-start">
<div className="relative w-37">
<select defaultValue="bestseller" aria-label="مرتب‌سازی" className="h-10 w-full appearance-none rounded-xl border border-[#E9EFF4] bg-white px-3 pr-10 text-right text-sm text-[#5B6A74]">
{SORT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
<Sort aria-hidden className="pointer-events-none absolute top-1/2 right-2.5 -translate-y-1/2 text-primary" color="currentColor" size={18} />
</div>
</div>
);
};
export default CategoryToolbar;