refactor: tab component
This commit is contained in:
@@ -3,8 +3,9 @@
|
|||||||
import Button from '@/components/button/PrimaryButton';
|
import Button from '@/components/button/PrimaryButton';
|
||||||
import CalendarIcon from '@/components/icons/CalendarIcon';
|
import CalendarIcon from '@/components/icons/CalendarIcon';
|
||||||
import LocationPinIcon from '@/components/icons/LocationPinIcon';
|
import LocationPinIcon from '@/components/icons/LocationPinIcon';
|
||||||
|
import PerformantTabRenderer from '@/components/tab/PerformantTabRenderer';
|
||||||
import TabContainer from '@/components/tab/TabContainer'
|
import TabContainer from '@/components/tab/TabContainer'
|
||||||
import { TabItem } from '@/components/tab/TabItem';
|
import { TabHeader } from '@/components/tab/TabHeader';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@@ -53,6 +54,50 @@ const currentOrders = [
|
|||||||
{ id: 2, quantity: 2, image: '/assets/images/food-preview.png' },
|
{ id: 2, quantity: 2, image: '/assets/images/food-preview.png' },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
address: '2...',
|
||||||
|
date: 'دوشنبه، 28 مرداد 1403',
|
||||||
|
price: '560,000',
|
||||||
|
items: [
|
||||||
|
{ id: 0, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 1, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 2, quantity: 2, image: '/assets/images/food-preview.png' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
address: '3...',
|
||||||
|
date: 'دوشنبه، 28 مرداد 1403',
|
||||||
|
price: '560,000',
|
||||||
|
items: [
|
||||||
|
{ id: 0, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 1, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 2, quantity: 2, image: '/assets/images/food-preview.png' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
address: '2...',
|
||||||
|
date: 'دوشنبه، 28 مرداد 1403',
|
||||||
|
price: '560,000',
|
||||||
|
items: [
|
||||||
|
{ id: 0, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 1, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 2, quantity: 2, image: '/assets/images/food-preview.png' },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
address: '3...',
|
||||||
|
date: 'دوشنبه، 28 مرداد 1403',
|
||||||
|
price: '560,000',
|
||||||
|
items: [
|
||||||
|
{ id: 0, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 1, quantity: 1, image: '/assets/images/food-preview.png' },
|
||||||
|
{ id: 2, quantity: 2, image: '/assets/images/food-preview.png' },
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
function OrdersIndex() {
|
function OrdersIndex() {
|
||||||
@@ -63,67 +108,67 @@ function OrdersIndex() {
|
|||||||
|
|
||||||
const firstTab = () => {
|
const firstTab = () => {
|
||||||
return currentOrders.map((order) => {
|
return currentOrders.map((order) => {
|
||||||
return (
|
return (
|
||||||
<div key={order.id} className='w-full px-[17px] mb-4 pt-8 pb-6 bg-white rounded-3xl'>
|
<div key={order.id} className='w-full px-[17px] mb-4 pt-8 pb-6 bg-white rounded-3xl'>
|
||||||
<div className="text-sm2">
|
<div className="text-sm2">
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<LocationPinIcon size={16} />
|
<LocationPinIcon size={16} />
|
||||||
<p>{order.address}</p>
|
<p>{order.address}</p>
|
||||||
</div>
|
|
||||||
<div className='mt-2 flex gap-2'>
|
|
||||||
<CalendarIcon size={16} />
|
|
||||||
<p>{order.date}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr className='mx-4 mt-6 text-disabled3 border-2' />
|
|
||||||
|
|
||||||
<div className='flex justify-between mt-6'>
|
|
||||||
<div className='flex justify-start gap-[9px]'>
|
|
||||||
{order.items.map((item) => {
|
|
||||||
return (
|
|
||||||
<span key={item.id} className='w-8 h-8 relative'>
|
|
||||||
<Image
|
|
||||||
priority
|
|
||||||
className='rounded-lg'
|
|
||||||
src={item.image}
|
|
||||||
width={32}
|
|
||||||
height={32}
|
|
||||||
alt="order image"
|
|
||||||
/>
|
|
||||||
<span className="absolute -bottom-3.5 -right-0.5 text-xs2 bg-white border border-border rounded-full w-[18px] h-[18px] text-center leading-4.5">
|
|
||||||
{item.quantity}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<div className='font-medium text-sm' dir='ltr'>
|
|
||||||
{order.price} T
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button className='mt-6 font-medium!'>پیگیری سفارش</Button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className='mt-2 flex gap-2'>
|
||||||
})
|
<CalendarIcon size={16} />
|
||||||
|
<p>{order.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className='mx-4 mt-6 text-disabled3 border-2' />
|
||||||
|
|
||||||
|
<div className='flex justify-between mt-6'>
|
||||||
|
<div className='flex justify-start gap-[9px]'>
|
||||||
|
{order.items.map((item) => {
|
||||||
|
return (
|
||||||
|
<span key={item.id} className='w-8 h-8 relative'>
|
||||||
|
<Image
|
||||||
|
priority
|
||||||
|
className='rounded-lg'
|
||||||
|
src={item.image}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
alt="order image"
|
||||||
|
/>
|
||||||
|
<span className="absolute -bottom-3.5 -right-0.5 text-xs2 bg-white border border-border rounded-full w-[18px] h-[18px] text-center leading-4.5">
|
||||||
|
{item.quantity}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<div className='font-medium text-sm' dir='ltr'>
|
||||||
|
{order.price} T
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button className='mt-6 font-medium!'>پیگیری سفارش</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<TabContainer
|
<TabContainer
|
||||||
className='h-[72px] !pt-3 !pb-2 overflow-y-hidden'>
|
|
||||||
<TabItem title={categories[0].title} imageSrc={categories[0].image}>
|
|
||||||
{firstTab()}
|
|
||||||
</TabItem>
|
|
||||||
<TabItem title={categories[0].title} imageSrc={categories[0].image}>
|
|
||||||
<div>empty</div>
|
|
||||||
</TabItem>
|
|
||||||
</TabContainer>
|
|
||||||
|
|
||||||
{/* First tab */}
|
className='h-[72px] !pt-3 !pb-2 overflow-y-hidden'>
|
||||||
{/* {selectedTab === 0 && firstTab()} */}
|
<TabHeader
|
||||||
|
viewRenderer={<PerformantTabRenderer rowHeight={310}>{firstTab()}</PerformantTabRenderer>}
|
||||||
|
title={categories[0].title} imageSrc={categories[0].image}>
|
||||||
|
</TabHeader>
|
||||||
|
<TabHeader
|
||||||
|
viewRenderer={<PerformantTabRenderer><div>empty</div><div></div></PerformantTabRenderer>}
|
||||||
|
title={categories[0].title} imageSrc={categories[0].image}>
|
||||||
|
</TabHeader>
|
||||||
|
</TabContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { ReactNode, useEffect, useRef } from 'react'
|
||||||
|
import { CellMeasurerCache, AutoSizer, CellMeasurer, List } from 'react-virtualized';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: Array<ReactNode>;
|
||||||
|
rowHeight?: number;
|
||||||
|
} & React.OlHTMLAttributes<HTMLUListElement>;
|
||||||
|
|
||||||
|
const cache = new CellMeasurerCache({
|
||||||
|
fixedWidth: true,
|
||||||
|
defaultHeight: 100,
|
||||||
|
});
|
||||||
|
|
||||||
|
function PerformantTabRenderer({ children, rowHeight = 100, ...rest }: Props) {
|
||||||
|
const listRef: React.Ref<List> | undefined = useRef(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
cache.clearAll();
|
||||||
|
listRef.current?.recomputeRowHeights();
|
||||||
|
}, 0); // 1 frame delay
|
||||||
|
|
||||||
|
const updateVh = () => {
|
||||||
|
const vh = window.innerHeight * 0.01;
|
||||||
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateVh();
|
||||||
|
window.addEventListener('resize', updateVh);
|
||||||
|
window.addEventListener('orientationchange', updateVh);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', updateVh);
|
||||||
|
window.removeEventListener('orientationchange', updateVh);
|
||||||
|
clearTimeout(timeout);
|
||||||
|
};
|
||||||
|
}, [children]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
{...rest}
|
||||||
|
style={{
|
||||||
|
height: (rowHeight * (children.length - 1)),
|
||||||
|
paddingTop: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AutoSizer>
|
||||||
|
{({ height, width }) => (
|
||||||
|
<List
|
||||||
|
style={{
|
||||||
|
direction: "rtl",
|
||||||
|
position: "relative",
|
||||||
|
WebkitOverflowScrolling: "touch",
|
||||||
|
overflowX: "auto",
|
||||||
|
overflowY: "hidden",
|
||||||
|
}} // optional
|
||||||
|
ref={listRef}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
rowCount={children.length}
|
||||||
|
rowHeight={cache.rowHeight}
|
||||||
|
rowRenderer={({ index, key, style, parent }) => (
|
||||||
|
<CellMeasurer
|
||||||
|
key={key}
|
||||||
|
cache={cache}
|
||||||
|
parent={parent}
|
||||||
|
columnIndex={0}
|
||||||
|
rowIndex={index}
|
||||||
|
>
|
||||||
|
{({ measure }) => (
|
||||||
|
<li style={style} dir='rtl'>
|
||||||
|
<div onLoad={measure}>
|
||||||
|
{children[index]}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</CellMeasurer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</AutoSizer>
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PerformantTabRenderer
|
||||||
@@ -1,18 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { ReactElement, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
import React, { ReactElement, ReactNode, useCallback, useState } from 'react'
|
||||||
import HorizontalScrollView from '../listview/HorizontalScrollView'
|
import HorizontalScrollView from '../listview/HorizontalScrollView'
|
||||||
import { TabItem, TabItemProps } from './TabItem';
|
import { TabHeader, TabItemProps } from './TabHeader';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import TabItemRenderer from './TabItemRenderer';
|
import TabHeaderRenderer from './TabHeaderRenderer';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { AutoSizer, CellMeasurer, CellMeasurerCache, List } from 'react-virtualized'
|
|
||||||
|
|
||||||
const cache = new CellMeasurerCache({
|
|
||||||
fixedWidth: true,
|
|
||||||
defaultHeight: 100,
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
itemRenderer?: (child: ReactNode, index: number) => ReactElement;
|
itemRenderer?: (child: ReactNode, index: number) => ReactElement;
|
||||||
@@ -22,42 +15,15 @@ type Props = {
|
|||||||
function TabContainer({ itemRenderer, children, ...restProps }: Props) {
|
function TabContainer({ itemRenderer, children, ...restProps }: Props) {
|
||||||
|
|
||||||
const [selectedTab, setSelectedTab] = useState(0);
|
const [selectedTab, setSelectedTab] = useState(0);
|
||||||
const listRef: React.Ref<List> | undefined = useRef(null)
|
|
||||||
|
|
||||||
const updateTab = useCallback((index: number) => {
|
const updateTab = useCallback((index: number) => {
|
||||||
setSelectedTab(index);
|
setSelectedTab(index);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTimeout(() => {
|
|
||||||
cache.clearAll();
|
|
||||||
listRef.current?.recomputeRowHeights();
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
const updateVh = () => {
|
|
||||||
const vh = window.innerHeight * 0.01;
|
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
||||||
};
|
|
||||||
|
|
||||||
updateVh();
|
|
||||||
window.addEventListener('resize', updateVh);
|
|
||||||
window.addEventListener('orientationchange', updateVh);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('resize', updateVh);
|
|
||||||
window.removeEventListener('orientationchange', updateVh);
|
|
||||||
};
|
|
||||||
|
|
||||||
}, [selectedTab, children]);
|
|
||||||
|
|
||||||
|
|
||||||
const items = React.Children.toArray(children).filter(
|
const items = React.Children.toArray(children).filter(
|
||||||
(child): child is ReactElement<TabItemProps> =>
|
(child): child is ReactElement<TabItemProps> =>
|
||||||
React.isValidElement(child) && child.type === TabItem
|
React.isValidElement(child) && child.type === TabHeader
|
||||||
);
|
);
|
||||||
const selectedTabChildren = React.Children.toArray(items[selectedTab].props.children);
|
|
||||||
console.log('children:', selectedTabChildren);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full'>
|
<div className='h-full'>
|
||||||
@@ -73,64 +39,44 @@ function TabContainer({ itemRenderer, children, ...restProps }: Props) {
|
|||||||
<div
|
<div
|
||||||
className='inline-flex gap-10'>
|
className='inline-flex gap-10'>
|
||||||
{items.map((item, index) => {
|
{items.map((item, index) => {
|
||||||
const { title, imageSrc }: TabItemProps = item.props;
|
const { title, imageSrc, icon }: TabItemProps = item.props;
|
||||||
return (
|
return (
|
||||||
<TabItemRenderer
|
<TabHeaderRenderer
|
||||||
key={index}
|
key={index}
|
||||||
className={''}
|
className={''}
|
||||||
onClick={() => updateTab(index)}
|
onClick={() => updateTab(index)}
|
||||||
>
|
>
|
||||||
<Image
|
{imageSrc &&
|
||||||
priority
|
<Image
|
||||||
src={imageSrc}
|
priority
|
||||||
width={24}
|
src={imageSrc}
|
||||||
height={24}
|
width={24}
|
||||||
alt="category image"
|
height={24}
|
||||||
/>
|
alt="category image"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
{icon &&
|
||||||
|
<span className={clsx(
|
||||||
|
'text-xs transition-all duration-150',
|
||||||
|
index === selectedTab ? 'stroke-black' : 'stroke-disabled-text'
|
||||||
|
)}>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
<span className={clsx(
|
<span className={clsx(
|
||||||
'text-xs transition-all duration-150',
|
'text-xs transition-all duration-150',
|
||||||
index === selectedTab ? 'text-black' : 'text-disabled-text'
|
index === selectedTab ? 'text-black' : 'text-disabled-text'
|
||||||
)}>
|
)}>
|
||||||
{title}
|
{title}
|
||||||
</span>
|
</span>
|
||||||
</TabItemRenderer>
|
</TabHeaderRenderer>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</HorizontalScrollView>
|
</HorizontalScrollView>
|
||||||
|
|
||||||
<div style={{ height: 'calc(var(--vh, 1vh) * 100 - 270px)' }} className='pe-2 pt-4 '>
|
<div>
|
||||||
<AutoSizer>
|
{items[selectedTab]?.props?.viewRenderer ?? <div></div>}
|
||||||
{({ height, width }) => (
|
|
||||||
<List
|
|
||||||
className='noscrollbar'
|
|
||||||
style={{ overflowX: 'hidden' }} // optional
|
|
||||||
ref={listRef}
|
|
||||||
width={width}
|
|
||||||
height={height}
|
|
||||||
rowCount={selectedTabChildren.length}
|
|
||||||
rowHeight={cache.rowHeight}
|
|
||||||
rowRenderer={({ index, key, style, parent }) => (
|
|
||||||
<CellMeasurer
|
|
||||||
key={key}
|
|
||||||
cache={cache}
|
|
||||||
parent={parent}
|
|
||||||
columnIndex={0}
|
|
||||||
rowIndex={index}
|
|
||||||
>
|
|
||||||
{({ measure, registerChild }) => (
|
|
||||||
<div ref={registerChild} style={style} dir='rtl'>
|
|
||||||
<div onLoad={measure}>
|
|
||||||
{selectedTabChildren[index]}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</CellMeasurer>
|
|
||||||
)}
|
|
||||||
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</AutoSizer>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import React, { ReactNode } from 'react';
|
||||||
|
|
||||||
|
export interface TabItemProps {
|
||||||
|
title: string;
|
||||||
|
imageSrc?: string;
|
||||||
|
icon?: ReactNode;
|
||||||
|
children?: ReactNode;
|
||||||
|
viewRenderer: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabHeader({ }: TabItemProps) {
|
||||||
|
return <></>; // it just renders children; logic handled by TabContainer
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ type Props = {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
} & React.HTMLAttributes<HTMLDivElement>;
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
function TabItemRendererComponent({ children, ...rest }: Props) {
|
function TabHeaderRendererComponent({ children, ...rest }: Props) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...rest}
|
{...rest}
|
||||||
@@ -16,8 +16,8 @@ function TabItemRendererComponent({ children, ...rest }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Memoize with shallow comparison of props
|
// Memoize with shallow comparison of props
|
||||||
const TabItemRenderer = React.memo(TabItemRendererComponent, (prevProps, nextProps) => {
|
const TabHeaderRenderer = React.memo(TabHeaderRendererComponent, (prevProps, nextProps) => {
|
||||||
return prevProps.className === nextProps.className && prevProps.children === nextProps.children;
|
return prevProps.className === nextProps.className && prevProps.children === nextProps.children;
|
||||||
});
|
});
|
||||||
|
|
||||||
export default TabItemRenderer;
|
export default TabHeaderRenderer;
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import React, { ReactNode } from 'react';
|
|
||||||
|
|
||||||
export interface TabItemProps {
|
|
||||||
title: string;
|
|
||||||
imageSrc: string;
|
|
||||||
children: ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TabItem({ children }: TabItemProps) {
|
|
||||||
return <>{children}</>; // it just renders children; logic handled by TabContainer
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user