responsive base v2
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
import Layout from '@/hoc/Layout'
|
import Layout from '@/hoc/Layout'
|
||||||
import { NextPage } from 'next'
|
import { NextPage } from 'next'
|
||||||
import {
|
import {
|
||||||
@@ -12,10 +14,14 @@ import Filters from '../components/Filters'
|
|||||||
import Sorts from '../components/Sorts'
|
import Sorts from '../components/Sorts'
|
||||||
import GridWrapper from '@/components/GridWrapper'
|
import GridWrapper from '@/components/GridWrapper'
|
||||||
import ProductCard from '@/components/ProductCard'
|
import ProductCard from '@/components/ProductCard'
|
||||||
|
import { Setting5 } from 'iconsax-react'
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
const Products: NextPage = () => {
|
const Products: NextPage = () => {
|
||||||
|
const [showMobileFilters, setShowMobileFilters] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-14 px-4 sm:px-8 md:px-20">
|
<div className="mt-14 px-4 sm:px-6 md:px-8 lg:px-20">
|
||||||
<Breadcrumb aria-label="breadcrumb">
|
<Breadcrumb aria-label="breadcrumb">
|
||||||
<BreadcrumbList className="text-sm text-muted-foreground">
|
<BreadcrumbList className="text-sm text-muted-foreground">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem>
|
||||||
@@ -32,8 +38,28 @@ const Products: NextPage = () => {
|
|||||||
</BreadcrumbList>
|
</BreadcrumbList>
|
||||||
</Breadcrumb>
|
</Breadcrumb>
|
||||||
|
|
||||||
<div className='mt-8 sm:mt-12 md:mt-14 flex flex-col lg:flex-row gap-4 sm:gap-6'>
|
{/* Mobile Filter Button */}
|
||||||
|
<div className='lg:hidden mt-4 flex justify-between items-center'>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowMobileFilters(true)}
|
||||||
|
className='flex items-center gap-2 px-3 py-2 border border-border rounded-lg bg-white shadow-sm'
|
||||||
|
>
|
||||||
|
<Setting5 size={18} color='#666666' />
|
||||||
|
<span className='text-sm text-[#666666]'>فیلترها</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='mt-6 sm:mt-8 md:mt-12 lg:mt-8 flex flex-col lg:flex-row gap-4 sm:gap-6'>
|
||||||
|
{/* Desktop Filters */}
|
||||||
<Filters />
|
<Filters />
|
||||||
|
|
||||||
|
{/* Mobile Filters Modal */}
|
||||||
|
<Filters
|
||||||
|
isMobile={true}
|
||||||
|
isOpen={showMobileFilters}
|
||||||
|
onClose={() => setShowMobileFilters(false)}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<Sorts />
|
<Sorts />
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,15 @@ import { getCategories } from '@/share/service/ShareService'
|
|||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||||
import CategoryTree from './CategoryTree'
|
import CategoryTree from './CategoryTree'
|
||||||
import { Category } from '@/share/types/SharedTypes'
|
import { Category } from '@/share/types/SharedTypes'
|
||||||
|
import Modal from '@/components/Modal'
|
||||||
|
|
||||||
const Filters: FC = () => {
|
type FiltersProps = {
|
||||||
|
isMobile?: boolean
|
||||||
|
isOpen?: boolean
|
||||||
|
onClose?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Filters: FC<FiltersProps> = ({ isMobile = false, isOpen = false, onClose }) => {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const pathname = usePathname()
|
const pathname = usePathname()
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
@@ -64,6 +71,11 @@ const Filters: FC = () => {
|
|||||||
if (rating) params.set('rating', String(rating))
|
if (rating) params.set('rating', String(rating))
|
||||||
else params.delete('rating')
|
else params.delete('rating')
|
||||||
router.push(`${pathname}?${params.toString()}`)
|
router.push(`${pathname}?${params.toString()}`)
|
||||||
|
|
||||||
|
// Close mobile modal after applying filters
|
||||||
|
if (isMobile && onClose) {
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearFilters = () => {
|
const clearFilters = () => {
|
||||||
@@ -77,8 +89,8 @@ const Filters: FC = () => {
|
|||||||
router.push(`${pathname}?${params.toString()}`)
|
router.push(`${pathname}?${params.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
const FilterContent = () => (
|
||||||
<div className='w-[270px] border border-border rounded-[10px] p-5'>
|
<>
|
||||||
<div className='flex items-center gap-2.5'>
|
<div className='flex items-center gap-2.5'>
|
||||||
<Setting5 size={24} color='#666666' />
|
<Setting5 size={24} color='#666666' />
|
||||||
<div className='text-[#666666]'>فیلترها</div>
|
<div className='text-[#666666]'>فیلترها</div>
|
||||||
@@ -169,6 +181,22 @@ const Filters: FC = () => {
|
|||||||
<Button variant='outline' onClick={clearFilters}>حذف</Button>
|
<Button variant='outline' onClick={clearFilters}>حذف</Button>
|
||||||
<Button onClick={applyFilters}>اعمال</Button>
|
<Button onClick={applyFilters}>اعمال</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<Modal open={isOpen} close={onClose || (() => { })} title="فیلترها" isHeader={true}>
|
||||||
|
<div className='max-h-[70vh] overflow-y-auto'>
|
||||||
|
<FilterContent />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='w-[270px] hidden lg:block border border-border rounded-[10px] p-5'>
|
||||||
|
<FilterContent />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +1,91 @@
|
|||||||
import { Sort } from 'iconsax-react'
|
import { Sort, ArrowDown2 } from 'iconsax-react'
|
||||||
import { FC } from 'react'
|
import { FC, useState, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const sortOptions = [
|
||||||
|
{ key: 'relevant', label: 'مرتبط ترین' },
|
||||||
|
{ key: 'most_visited', label: 'پربازدیدترین' },
|
||||||
|
{ key: 'newest', label: 'جدیدترین' },
|
||||||
|
{ key: 'best_selling', label: 'پرفروش ترین' },
|
||||||
|
{ key: 'cheapest', label: 'ارزانترین' },
|
||||||
|
{ key: 'most_expensive', label: 'گرانترین' },
|
||||||
|
{ key: 'customer_suggestion', label: 'پیشنهاد خریداران' },
|
||||||
|
]
|
||||||
|
|
||||||
const Sorts: FC = () => {
|
const Sorts: FC = () => {
|
||||||
|
const [selectedSort, setSelectedSort] = useState('newest')
|
||||||
|
const [showDropdown, setShowDropdown] = useState(false)
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const selectedOption = sortOptions.find(option => option.key === selectedSort)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
setShowDropdown(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDropdown) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside)
|
||||||
|
}
|
||||||
|
}, [showDropdown])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-between items-center text-[#999999] text-sm border-b border-border pb-5'>
|
<div className='flex justify-between items-center text-[#999999] text-sm border-b border-border pb-5'>
|
||||||
<div className='flex items-center gap-4'>
|
{/* Desktop Version */}
|
||||||
|
<div className='hidden lg:flex items-center gap-4'>
|
||||||
<div className='flex gap-2 items-center'>
|
<div className='flex gap-2 items-center'>
|
||||||
<Sort size={24} color='#333333' />
|
<Sort size={24} color='#333333' />
|
||||||
<div className='text-sm text-[#333333]'>مرتب سازی:</div>
|
<div className='text-sm text-[#333333]'>مرتب سازی:</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='cursor-pointer'>
|
{sortOptions.map((option) => (
|
||||||
مرتبط ترین
|
<div
|
||||||
</div>
|
key={option.key}
|
||||||
<div className='cursor-pointer'>
|
className={`cursor-pointer ${selectedSort === option.key ? 'text-primary' : ''}`}
|
||||||
پربازدیدترین
|
onClick={() => setSelectedSort(option.key)}
|
||||||
</div>
|
>
|
||||||
<div className='text-primary'>
|
{option.label}
|
||||||
جدیدترین
|
|
||||||
</div>
|
|
||||||
<div className='cursor-pointer'>
|
|
||||||
پرفروش ترین
|
|
||||||
</div>
|
|
||||||
<div className='cursor-pointer'>
|
|
||||||
ارزانترین
|
|
||||||
</div>
|
|
||||||
<div className='cursor-pointer'>
|
|
||||||
گرانترین
|
|
||||||
</div>
|
|
||||||
<div className='cursor-pointer'>
|
|
||||||
پیشنهاد خریداران
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* Mobile Version */}
|
||||||
|
<div className='lg:hidden flex items-center gap-2 relative' ref={dropdownRef}>
|
||||||
|
<Sort size={20} color='#333333' />
|
||||||
|
<div className='text-sm text-[#333333]'>مرتب سازی:</div>
|
||||||
|
<div
|
||||||
|
className='flex items-center gap-1 cursor-pointer bg-gray-50 px-3 py-1.5 rounded-lg'
|
||||||
|
onClick={() => setShowDropdown(!showDropdown)}
|
||||||
|
>
|
||||||
|
<span className='text-sm text-[#333333]'>{selectedOption?.label}</span>
|
||||||
|
<ArrowDown2 size={16} color='#333333' />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showDropdown && (
|
||||||
|
<div className='absolute top-full right-0 mt-2 bg-white border border-border rounded-lg shadow-lg z-10 min-w-[180px]'>
|
||||||
|
{sortOptions.map((option) => (
|
||||||
|
<div
|
||||||
|
key={option.key}
|
||||||
|
className={`px-4 py-3 cursor-pointer text-sm hover:bg-gray-50 ${selectedSort === option.key ? 'text-primary bg-blue-50' : 'text-[#333333]'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedSort(option.key)
|
||||||
|
setShowDropdown(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{option.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className='text-sm'>
|
||||||
100 کالا
|
100 کالا
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
const [search, setSearch] = useState<string>('')
|
const [search, setSearch] = useState<string>('')
|
||||||
|
|
||||||
const inputClass = clx(
|
const inputClass = clx(
|
||||||
'w-full h-10 sm:h-12 text-black block px-3 sm:px-4 text-xs sm:text-sm rounded-xl border border-[#D0D0D0]',
|
'w-full h-10 text-black block px-4 text-xs rounded-xl border border-[#D0D0D0]',
|
||||||
props.readOnly && 'bg-gray-100 border-0 text-description',
|
props.readOnly && 'bg-gray-100 border-0 text-description',
|
||||||
props.variant === 'search' && 'bg-[#EEF0F7] border-0 pr-10',
|
props.variant === 'search' && 'bg-[#EEF0F7] border-0 pr-10',
|
||||||
props.className
|
props.className
|
||||||
@@ -80,7 +80,7 @@ const Input: FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<label className='text-xs sm:text-sm text-black'>
|
<label className='text-sm text-black'>
|
||||||
{props.label}
|
{props.label}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user