add: dropdown component

This commit is contained in:
Mahyar Khanbolooki
2025-07-11 18:04:26 +03:30
parent 20e8be4d09
commit cec8ac0041
6 changed files with 173 additions and 16 deletions
+54 -11
View File
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useMemo, useState } from "react";
import { MouseEventHandler, useCallback, useMemo, useState } from "react";
import SearchBox from "@/components/input/SearchBox";
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
@@ -11,9 +11,9 @@ import VerticalScrollView from "@/components/listview/VerticalScrollView";
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
import React from "react";
import MenuItem from "@/components/listview/MenuItem";
import CloseIcon from "@/components/icons/CloseIcon";
import Button from "@/components/button/PrimaryButton";
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
import MultiOption, { DropdownOption } from "@/components/dropdown/MultiOption";
const categories = new Array(13).fill({ title: "خوراک", icon: "" });
@@ -118,10 +118,26 @@ const foods = [
}
];
const sortings = [
'محبوب ترین',
'بالاترین امتیاز',
'جدید ترین',
'ارزان ترین',
'گران ترین',
]
const contents: Array<DropdownOption> = [
{ id: '0', title: 'گیاهی' },
{ id: '1', title: 'حیوانی' }
]
const MenuIndex = () => {
const [search, setSearch] = useState("");
const [selectedCategory, setSelectedCategory] = useState(0);
const [filterModal, setFilterModal] = useState(false);
const [sortingModal, setSortingModal] = useState(false);
const [sorting, setSorting] = useState(0);
const [selectedContentId, setSelectedContentId] = useState('0');
const updateSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setSearch(e.target.value);
@@ -135,6 +151,17 @@ const MenuIndex = () => {
setFilterModal((state) => !state);
}, []);
const toggleSortingModal = useCallback(() => {
setSortingModal((state) => !state);
}, []);
const changeSorting = (index: number) => {
setSorting(() => index);
}
const changeSelectedContent = (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => {
setSelectedContentId(() => contents[index]?.id || contents[0].id);
}
const filteredReceiptItems = useMemo(() => {
const lowerSearch = search.toLowerCase();
@@ -178,7 +205,7 @@ const MenuIndex = () => {
<EqualizerIcon />
<span className="text-xs leading-5">فیلتر بر اساس</span>
</button>
<button className="rounded-lg h-8 bg-white p-1.5">
<button onClick={toggleSortingModal} className="rounded-lg h-8 bg-white p-1.5">
<TextAlignIcon />
</button>
</div>
@@ -193,15 +220,16 @@ const MenuIndex = () => {
</VerticalScrollView>
</section>
<AnimatedBottomSheet visible={filterModal} changeDelay="150" onClick={toggleFilterModal}>
<div className="px-8 flex justify-between">
<div>فیلتر ها</div>
<div onClick={toggleFilterModal} className="bg-white/38 w-8 h-8 rounded-full p-1.5">
<CloseIcon size={20} />
</div>
<AnimatedBottomSheet title="فیلتر ها" visible={filterModal} changeDelay="150" onClick={toggleFilterModal}>
<div className="ps-8.5 pe-[31px] mt-[89px]">
<MultiOption
title="محتویات"
options={contents}
selectedId={selectedContentId}
onSelectionChange={changeSelectedContent} />
</div>
<hr className="text-white/40 mb-7 mt-12" />
<div className="px-9 flex justify-between gap-[22px]">
<div className="px-9 pt-13 flex justify-between gap-[22px]">
<hr className="text-white/40 mb-7 mt-12" />
<div className="w-full">
<Button>اعمال فیلتر</Button>
</div>
@@ -210,6 +238,21 @@ const MenuIndex = () => {
</div>
</div>
</AnimatedBottomSheet>
<AnimatedBottomSheet title="مرتب کردن بر اساس" visible={sortingModal} changeDelay="150" onClick={toggleSortingModal}>
<div className="px-8.5 py-10 justify-between">
{sortings.map((v, i) => {
return (
<div key={i}>
<div onClick={() => changeSorting(i)} className="text-sm2 font-medium cursor-pointer">
{v}
</div>
{i < sortings.length - 1 && <hr className="text-white/40 mb-4 mt-4" />}
</div>
)
})}
</div>
</AnimatedBottomSheet>
</div>
);
};
@@ -15,7 +15,10 @@ const AnimatedBottomSheet = (props: Props) => {
className={clsx(
'absolute -bottom-full left-0 w-full bg-white/64 rounded-t-4xl pt-[39px] pb-6',
'data-[visible=true]:bottom-0 transition-all duration-150'
)}>
)}
style={{
backdropFilter: 'blur(44px)'
}}>
<div className="ps-[31px] pe-[33px] flex justify-between">
<div className='text-lg font-medium text-black leading-8 mt-2.5'>{props.title}</div>
<div onClick={props.onClick} className="bg-white/38 w-8 h-8 rounded-full p-1.5">
+79
View File
@@ -0,0 +1,79 @@
import React, { useState } from 'react'
import ArrowDownIcon from '../icons/ArrowDownIcon';
export interface DropdownOption {
id: string;
title: string;
}
type Props = {
title: string;
options: Array<DropdownOption>;
expanded?: boolean;
selectedId: string;
onSelectionChange: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, index: number) => void,
} & React.HTMLAttributes<HTMLDivElement>
function MultiOption({ title, options, expanded, selectedId, onSelectionChange, ...props }: Props) {
const [expand, setExpand] = useState(expanded);
const toggleExpand = () => {
setExpand((state) => !state);
}
return (
<div className="relative" {...props}>
<div
className="flex w-full h-11 items-center justify-end gap-2 px-3 py-4 relative bg-white/29 rounded-xl border border-solid border-neutral-200 cursor-pointer focus:outline-none focus:ring-2 focus:ring-black"
tabIndex={0}
onClick={toggleExpand}
role='combobox'
aria-expanded={expanded}
aria-haspopup='listbox'
aria-controls=''
aria-label={title}>
<label
htmlFor='content-select'
className='inline-flex flex-col items-end justify-center px-1 py-0 absolute -top-6 right-0 z-[2]'>
<span className='relative w-fit mt-[-1px] text-xs tracking-[0] leading-4 whitespace-nowrap'>
{title}
</span>
</label>
<div className='w-full text-sm2'>
{options.filter(x => x.id === selectedId)[0]?.title ?? ''}
</div>
<ArrowDownIcon />
</div>
{expand &&
<div
className='absolute top-full left-0 w-full mt-1 bg-white rounded-xl border border-solid border-neutral-200 shadow-lg z-10'
role='listbox'
aria-label={`${title} options`}>
{options.map((v, i) => {
return (
<div
onClick={(e) => {setExpand(() => false); onSelectionChange(e, i) }}
key={v.id}
data-selected={v.id === selectedId}
className='flex items-center justify-end px-3 py-3 cursor-pointer hover:bg-gray-50 first:rounded-t-xl last:rounded-b-xl'
tabIndex={0}
role='option'
aria-selected>
<span
className='text-sm2 tracking-[0.13px] leading-5 w-full'
>
{v.title}
</span>
</div>
)
})}
</div>
}
</div>
)
}
export default MultiOption
+34
View File
@@ -0,0 +1,34 @@
import React from 'react';
interface ArrowDownIconProps extends React.SVGProps<SVGSVGElement> {
width?: number;
height?: number;
strokeColor?: string;
}
const ArrowDownIcon: React.FC<ArrowDownIconProps> = ({
width = 16,
height = 16,
strokeColor = '#999999',
...props
}) => (
<svg
width={width}
height={height}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M13.2797 5.96655L8.93306 10.3132C8.41973 10.8266 7.57973 10.8266 7.06639 10.3132L2.71973 5.96655"
stroke={strokeColor}
strokeWidth="2"
strokeMiterlimit="10"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default ArrowDownIcon;
@@ -6,7 +6,7 @@ type Props = {
function CategoryItemRenderer({ children, ...rest }: Props) {
return (
<div className={`${rest.className} transition-all duration-200 ease-out border-2 border-solid border-white overflow-hidden rounded-xl backdrop-blur-md bg-white/40`}>
<div className={`${rest.className} cursor-pointer transition-all duration-200 ease-out border-2 border-solid border-white overflow-hidden rounded-xl backdrop-blur-md bg-white/40`}>
<div
{...rest}
className="rounded-normal w-[88px] h-[88px] flex flex-col justify-center items-center gap-2"
+1 -3
View File
@@ -8,9 +8,7 @@ import HeartIcon from '../icons/HeartIcon'
import { useParams, usePathname } from 'next/navigation'
import HomeIcon from '../icons/HomeIcon'
type Props = object
function BottomNavBar({ }: Props) {
function BottomNavBar() {
const params = useParams();
const { name } = params;
const pathname = usePathname();