add: dropdown component
This commit is contained in:
+54
-11
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { MouseEventHandler, useCallback, useMemo, useState } from "react";
|
||||||
import SearchBox from "@/components/input/SearchBox";
|
import SearchBox from "@/components/input/SearchBox";
|
||||||
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
|
import CategoryItemRenderer from "@/components/listview/CategoryItemRenderer";
|
||||||
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
import HorizontalScrollView from "@/components/listview/HorizontalScrollView";
|
||||||
@@ -11,9 +11,9 @@ import VerticalScrollView from "@/components/listview/VerticalScrollView";
|
|||||||
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
import MenuItemRenderer from "@/components/listview/MenuItemRenderer";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import MenuItem from "@/components/listview/MenuItem";
|
import MenuItem from "@/components/listview/MenuItem";
|
||||||
import CloseIcon from "@/components/icons/CloseIcon";
|
|
||||||
import Button from "@/components/button/PrimaryButton";
|
import Button from "@/components/button/PrimaryButton";
|
||||||
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
|
import AnimatedBottomSheet from "@/components/bottomsheet/AnimatedBottomSheet";
|
||||||
|
import MultiOption, { DropdownOption } from "@/components/dropdown/MultiOption";
|
||||||
|
|
||||||
const categories = new Array(13).fill({ title: "خوراک", icon: "" });
|
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 MenuIndex = () => {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [selectedCategory, setSelectedCategory] = useState(0);
|
const [selectedCategory, setSelectedCategory] = useState(0);
|
||||||
const [filterModal, setFilterModal] = useState(false);
|
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>) => {
|
const updateSearch = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setSearch(e.target.value);
|
setSearch(e.target.value);
|
||||||
@@ -135,6 +151,17 @@ const MenuIndex = () => {
|
|||||||
setFilterModal((state) => !state);
|
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 filteredReceiptItems = useMemo(() => {
|
||||||
const lowerSearch = search.toLowerCase();
|
const lowerSearch = search.toLowerCase();
|
||||||
@@ -178,7 +205,7 @@ const MenuIndex = () => {
|
|||||||
<EqualizerIcon />
|
<EqualizerIcon />
|
||||||
<span className="text-xs leading-5">فیلتر بر اساس</span>
|
<span className="text-xs leading-5">فیلتر بر اساس</span>
|
||||||
</button>
|
</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 />
|
<TextAlignIcon />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,15 +220,16 @@ const MenuIndex = () => {
|
|||||||
</VerticalScrollView>
|
</VerticalScrollView>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<AnimatedBottomSheet visible={filterModal} changeDelay="150" onClick={toggleFilterModal}>
|
<AnimatedBottomSheet title="فیلتر ها" visible={filterModal} changeDelay="150" onClick={toggleFilterModal}>
|
||||||
<div className="px-8 flex justify-between">
|
<div className="ps-8.5 pe-[31px] mt-[89px]">
|
||||||
<div>فیلتر ها</div>
|
<MultiOption
|
||||||
<div onClick={toggleFilterModal} className="bg-white/38 w-8 h-8 rounded-full p-1.5">
|
title="محتویات"
|
||||||
<CloseIcon size={20} />
|
options={contents}
|
||||||
</div>
|
selectedId={selectedContentId}
|
||||||
|
onSelectionChange={changeSelectedContent} />
|
||||||
</div>
|
</div>
|
||||||
<hr className="text-white/40 mb-7 mt-12" />
|
<div className="px-9 pt-13 flex justify-between gap-[22px]">
|
||||||
<div className="px-9 flex justify-between gap-[22px]">
|
<hr className="text-white/40 mb-7 mt-12" />
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Button>اعمال فیلتر</Button>
|
<Button>اعمال فیلتر</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -210,6 +238,21 @@ const MenuIndex = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AnimatedBottomSheet>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ const AnimatedBottomSheet = (props: Props) => {
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
'absolute -bottom-full left-0 w-full bg-white/64 rounded-t-4xl pt-[39px] pb-6',
|
'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'
|
'data-[visible=true]:bottom-0 transition-all duration-150'
|
||||||
)}>
|
)}
|
||||||
|
style={{
|
||||||
|
backdropFilter: 'blur(44px)'
|
||||||
|
}}>
|
||||||
<div className="ps-[31px] pe-[33px] flex justify-between">
|
<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 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">
|
<div onClick={props.onClick} className="bg-white/38 w-8 h-8 rounded-full p-1.5">
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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) {
|
function CategoryItemRenderer({ children, ...rest }: Props) {
|
||||||
return (
|
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
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
className="rounded-normal w-[88px] h-[88px] flex flex-col justify-center items-center gap-2"
|
className="rounded-normal w-[88px] h-[88px] flex flex-col justify-center items-center gap-2"
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ import HeartIcon from '../icons/HeartIcon'
|
|||||||
import { useParams, usePathname } from 'next/navigation'
|
import { useParams, usePathname } from 'next/navigation'
|
||||||
import HomeIcon from '../icons/HomeIcon'
|
import HomeIcon from '../icons/HomeIcon'
|
||||||
|
|
||||||
type Props = object
|
function BottomNavBar() {
|
||||||
|
|
||||||
function BottomNavBar({ }: Props) {
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const { name } = params;
|
const { name } = params;
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|||||||
Reference in New Issue
Block a user