table like gmail and openn and close new messsage with animation +...
This commit is contained in:
@@ -68,6 +68,81 @@ const Table = <T extends RowDataType>({
|
|||||||
return selectedRows.some((row) => row.id === item.id);
|
return selectedRows.some((row) => row.id === item.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Gmail-style layout برای وقتی header نمایش داده نمیشود
|
||||||
|
const renderGmailStyleRows = () => {
|
||||||
|
const hasActions = (actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions));
|
||||||
|
const roundedClass = hasActions ? 'rounded-b-2xl' : 'rounded-2xl';
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className={`bg-white ${roundedClass} overflow-hidden`}>
|
||||||
|
{Array.from({ length: emptyRowsCount }).map((_, index) => (
|
||||||
|
<div key={index} className={`h-[48px] bg-gray-100 animate-pulse ${index !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={`bg-white ${roundedClass} py-6 md:py-8 text-center text-gray-500`}>
|
||||||
|
{noDataMessage}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`bg-white ${roundedClass} overflow-hidden`}>
|
||||||
|
{data.map((item, rowIndex) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={`w-full min-h-[48px] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}
|
||||||
|
onClick={() => handleRowClick(item)}
|
||||||
|
>
|
||||||
|
{selectable && (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 relative z-[5]"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={isRowSelected(item)}
|
||||||
|
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
{/* محتوای اصلی - ستون اول */}
|
||||||
|
<div className="flex items-center mb-0.5 text-sm font-medium">
|
||||||
|
{columns[0]?.render ? columns[0].render(item) : (item[columns[0]?.key] as React.ReactNode)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* اطلاعات اضافی - سه ستون آخر در یک خط */}
|
||||||
|
{columns.length > 1 && (
|
||||||
|
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||||
|
{columns.slice(1).map((col) => (
|
||||||
|
<div key={col.key} className="truncate">
|
||||||
|
{col.render ? col.render(item) : (item[col.key] as React.ReactNode)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{rowActions && (
|
||||||
|
<div
|
||||||
|
className="flex-shrink-0 relative z-[5]"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<RowActionsDropdown actions={rowActions(item)} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderTableBody = () => {
|
const renderTableBody = () => {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -165,6 +240,53 @@ const Table = <T extends RowDataType>({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// اگر header نشان داده نمیشود، از Gmail-style layout فقط در موبایل استفاده کن
|
||||||
|
if (!showHeader) {
|
||||||
|
return (
|
||||||
|
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
|
||||||
|
{/* نسخه موبایل - Gmail style */}
|
||||||
|
<div className={`md:hidden`}>
|
||||||
|
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
|
||||||
|
<div className="h-[64px] bg-white flex items-center px-4 rounded-t-2xl border-b border-[#EAEDF5]">
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
{actions}
|
||||||
|
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{renderGmailStyleRows()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* نسخه دسکتاپ - Table معمولی */}
|
||||||
|
<div className={`hidden md:block`}>
|
||||||
|
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
|
||||||
|
<div className="h-[74px] bg-white flex items-center px-6 rounded-t-3xl">
|
||||||
|
<div className='flex items-center gap-4'>
|
||||||
|
{actions}
|
||||||
|
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={`overflow-x-auto overflow-hidden ${(actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions)) ? 'rounded-b-3xl' : 'rounded-3xl'} bg-white`}>
|
||||||
|
<table className="w-full text-sm border-collapse min-w-[600px]">
|
||||||
|
<tbody>
|
||||||
|
{renderTableBody()}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{pagination && (
|
||||||
|
<Pagination
|
||||||
|
currentPage={pagination.currentPage}
|
||||||
|
totalPages={pagination.totalPages}
|
||||||
|
onPageChange={pagination.onPageChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
|
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
|
||||||
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
|
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, useState } from 'react'
|
import { FC, useState, useEffect } from 'react'
|
||||||
import { CloseCircle } from 'iconsax-react'
|
import { CloseCircle } from 'iconsax-react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useSharedStore } from '@/shared/store/sharedStore'
|
import { useSharedStore } from '@/shared/store/sharedStore'
|
||||||
@@ -12,22 +12,53 @@ const NewMessage: FC = () => {
|
|||||||
const { t } = useTranslation('global')
|
const { t } = useTranslation('global')
|
||||||
const [value, setValue] = useState('');
|
const [value, setValue] = useState('');
|
||||||
const { setOpenNewMessage, openNewMessage } = useSharedStore()
|
const { setOpenNewMessage, openNewMessage } = useSharedStore()
|
||||||
|
const [isClosing, setIsClosing] = useState(false)
|
||||||
|
const [shouldRender, setShouldRender] = useState(false)
|
||||||
|
const [isOpening, setIsOpening] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (openNewMessage) {
|
||||||
|
setShouldRender(true)
|
||||||
|
setIsClosing(false)
|
||||||
|
setIsOpening(true)
|
||||||
|
// شروع انیمیشن باز شدن بعد از رندر
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsOpening(false)
|
||||||
|
}, 50)
|
||||||
|
} else if (shouldRender) {
|
||||||
|
setIsClosing(true)
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setShouldRender(false)
|
||||||
|
setIsClosing(false)
|
||||||
|
}, 300)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [openNewMessage, shouldRender])
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
setIsClosing(true)
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpenNewMessage(false)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
{openNewMessage && (
|
{shouldRender && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className='fixed inset-0 xl:bg-transparent bg-black/50 z-[9998]'
|
className={`fixed inset-0 xl:bg-transparent bg-black/50 z-[9998] transition-opacity duration-300 ${isClosing ? 'opacity-0' : isOpening ? 'opacity-0' : 'opacity-100'
|
||||||
onClick={() => setOpenNewMessage(false)}
|
}`}
|
||||||
|
onClick={handleClose}
|
||||||
/>
|
/>
|
||||||
<div className='fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 xl:left-8 xl:right-auto xl:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] transition-all duration-300 p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto'>
|
<div className={`fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 xl:left-8 xl:right-auto xl:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
|
||||||
|
}`}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className='flex justify-between items-center mb-6 md:mb-8'>
|
<div className='flex justify-between items-center mb-6 md:mb-8'>
|
||||||
<div className='text-lg'>
|
<div className='text-lg'>
|
||||||
{t('new_message.title')}
|
{t('new_message.title')}
|
||||||
</div>
|
</div>
|
||||||
<div onClick={() => setOpenNewMessage(false)} className='cursor-pointer p-1'>
|
<div onClick={handleClose} className='cursor-pointer p-1'>
|
||||||
<CloseCircle size={20} className='md:w-6 md:h-6' color='#292D32' />
|
<CloseCircle size={20} className='md:w-6 md:h-6' color='#292D32' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ const SideBar: FC = () => {
|
|||||||
}
|
}
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
'fixed xl:flex flex-1 translate-x-[400px] xl:translate-x-0 transition-all ease-in-out opacity-0 invisible xl:visible xl:opacity-100 xl:right-2 xl:md:right-4 right-0 xl:top-2 xl:md:top-4 top-0 xl:bottom-2 xl:md:bottom-4 bottom-0 xl:rounded-[20px] xl:md:rounded-[32px] w-[250px] sm:w-[280px] xl:w-[250px] bg-white flex-col py-8 md:py-10 xl:py-12',
|
'fixed xl:flex flex translate-x-[400px] xl:translate-x-0 transition-all ease-in-out opacity-0 invisible xl:visible xl:opacity-100 xl:right-2 xl:md:right-4 right-0 xl:top-2 xl:md:top-4 top-0 xl:bottom-2 xl:md:bottom-4 bottom-0 xl:rounded-[20px] xl:md:rounded-[32px] w-[250px] sm:w-[280px] xl:w-[250px] bg-white flex-col py-8 md:py-10 xl:py-12',
|
||||||
openSidebar && 'opacity-100 visible -translate-x-[0px] block z-40'
|
openSidebar && 'opacity-100 visible -translate-x-[0px] flex z-40'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className='flex justify-center px-4'>
|
<div className='flex justify-center px-4'>
|
||||||
@@ -50,7 +50,7 @@ const SideBar: FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex-1 flex flex-col h-full overflow-y-auto no-scrollbar'>
|
<div className='flex-1 flex flex-col h-full overflow-y-auto no-scrollbar min-h-0'>
|
||||||
<div className='mt-8 md:mt-10 px-8 md:px-12 text-[#C3C7DD] text-sm font-bold'>
|
<div className='mt-8 md:mt-10 px-8 md:px-12 text-[#C3C7DD] text-sm font-bold'>
|
||||||
{t('sidebar.menu')}
|
{t('sidebar.menu')}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user