From 4c869c42c6894c404d25bc6750f3c4615915482a Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Tue, 17 Jun 2025 20:52:40 +0330 Subject: [PATCH] reponsive and selective actions and row actioin + pagination --- src/components/Button.tsx | 54 ++++---- src/components/DefaultTableSkeleton.tsx | 2 +- src/components/Filters.tsx | 8 +- src/components/Input.tsx | 1 - src/components/Pagination.tsx | 109 +++++++++++++++ src/components/RowActionsDropdown.tsx | 117 ++++++++++++++++ src/components/Table.tsx | 169 ++++++++++++++++------- src/components/Tabs.tsx | 10 +- src/components/Td.tsx | 2 +- src/components/newMessage/NewMessage.tsx | 61 ++++---- src/components/types/TableTypes.ts | 11 ++ src/index.css | 24 ++-- src/langs/fa.json | 31 ++++- src/pages/Trash/List.tsx | 107 ++++++++++---- src/pages/archive/List.tsx | 108 +++++++++++---- src/pages/draft/List.tsx | 104 +++++++++----- src/pages/home/Home.tsx | 29 +++- src/pages/received/List.tsx | 93 +++++++++++-- src/pages/sent/List.tsx | 36 +++-- src/pages/setting/Setting.tsx | 24 ++-- src/shared/Header.tsx | 10 +- src/shared/Main.tsx | 8 +- src/shared/SideBar.tsx | 27 ++-- src/shared/SideBarItem.tsx | 11 +- 24 files changed, 869 insertions(+), 287 deletions(-) create mode 100644 src/components/Pagination.tsx create mode 100644 src/components/RowActionsDropdown.tsx diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 9b33f66..f6c9478 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -1,40 +1,40 @@ -import { ButtonHTMLAttributes, FC, memo, ReactNode } from 'react' -import MoonLoader from "react-spinners/MoonLoader" -import { XOR } from '../helpers/types' +import { FC, ReactNode } from 'react' import { clx } from '../helpers/utils' -type Props = { +interface ButtonProps { + label?: string; + children?: ReactNode; className?: string; - isLoading?: boolean, - percentage?: number, -} & ButtonHTMLAttributes & - XOR<{ children: ReactNode }, { label: string }>; + loading?: boolean; + onClick?: () => void; + type?: 'button' | 'submit' | 'reset'; + disabled?: boolean; +} -const Button: FC = memo((props: Props) => { +const Button: FC = (props) => { const buttonClass = clx( - 'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary text-white w-full', - props.disabled && 'cursor-not-allowed opacity-60', + 'w-full bg-primary text-white rounded-xl font-normal text-xs md:text-sm h-10 px-3 md:px-4 transition-all duration-200 hover:bg-opacity-90 disabled:opacity-50 disabled:cursor-not-allowed', props.className - ); + ) return ( - ) -}) +} export default Button \ No newline at end of file diff --git a/src/components/DefaultTableSkeleton.tsx b/src/components/DefaultTableSkeleton.tsx index c230eb7..9e7257a 100644 --- a/src/components/DefaultTableSkeleton.tsx +++ b/src/components/DefaultTableSkeleton.tsx @@ -12,7 +12,7 @@ const DefaultTableSkeleton: FC = ({ tdCount, trCount = 5 }) => { { Array.from({ length: trCount }).map((_, rowIndex) => ( - + { Array.from({ length: tdCount }).map((_, colIndex) => ( diff --git a/src/components/Filters.tsx b/src/components/Filters.tsx index 5bf20a2..b4494c6 100644 --- a/src/components/Filters.tsx +++ b/src/components/Filters.tsx @@ -44,8 +44,8 @@ const Filters: FC = ({ fields, onChange, initialValues = {}, - className = "mt-10 flex justify-between items-center", - fieldClassName = "flex gap-4", + className = "mt-6 md:mt-8 xl:mt-10 flex flex-col md:flex-row justify-between items-start md:items-center gap-4", + fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto", searchField = "search" // پیش‌فرض فیلد سرچ با نام search }) => { const [filters, setFilters] = useState({}); @@ -111,7 +111,7 @@ const Filters: FC = ({ key={field.name} placeholder={field.placeholder} variant="search" - className="max-w-[200px]" + className="w-full md:max-w-[200px]" value={currentValue || field.defaultValue || ''} onChange={(e) => handleInputChange(field.name, e)} /> @@ -129,7 +129,7 @@ const Filters: FC = ({ {otherFields.map(renderField)} {searchFieldObj && ( -
+
{renderField(searchFieldObj)}
)} diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 1042666..b20c24d 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -96,7 +96,6 @@ const Input: FC = (props: Props) => { { props.type === 'password' && setShowPassword((oldValue) => !oldValue)} size={20} color='#8C90A3' className='absolute top-0 bottom-0 cursor-pointer my-auto left-3' /> - // setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' /> } { diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..d381ceb --- /dev/null +++ b/src/components/Pagination.tsx @@ -0,0 +1,109 @@ +import React from "react"; + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +const Pagination: React.FC = ({ + currentPage, + totalPages, + onPageChange, +}) => { + // اگر فقط یک صفحه داریم، pagination نمایش داده نمی‌شود + if (totalPages <= 1) return null; + + const getPageNumbers = () => { + const pageNumbers: (number | string)[] = []; + const maxVisiblePages = window.innerWidth < 768 ? 3 : 5; // تعداد کمتر برای موبایل + + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + // نمایش صفحه اول + pageNumbers.push(1); + + if (currentPage > 3) { + pageNumbers.push("..."); + } + + // صفحات میانی + const start = Math.max(2, Math.min(currentPage - 1, totalPages - 3)); + const end = Math.min(totalPages - 1, Math.max(currentPage + 1, 4)); + + for (let i = start; i <= end; i++) { + if (!pageNumbers.includes(i)) { + pageNumbers.push(i); + } + } + + if (currentPage < totalPages - 2) { + pageNumbers.push("..."); + } + + // نمایش صفحه آخر + if (!pageNumbers.includes(totalPages)) { + pageNumbers.push(totalPages); + } + } + + return pageNumbers; + }; + + const pageNumbers = getPageNumbers(); + + return ( +
+ {/* دکمه صفحه قبل */} + + + {/* شماره صفحات */} +
+ {pageNumbers.map((page, index) => + typeof page === "number" ? ( + + ) : ( + + {page} + + ) + )} +
+ + {/* دکمه صفحه بعد */} + +
+ ); +}; + +export default Pagination; \ No newline at end of file diff --git a/src/components/RowActionsDropdown.tsx b/src/components/RowActionsDropdown.tsx new file mode 100644 index 0000000..249cfff --- /dev/null +++ b/src/components/RowActionsDropdown.tsx @@ -0,0 +1,117 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { createPortal } from 'react-dom'; +import { More } from 'iconsax-react'; + +export interface RowActionItem { + label: string; + icon?: React.ReactNode; + onClick: () => void; + className?: string; +} + +interface RowActionsDropdownProps { + actions: RowActionItem[]; + className?: string; +} + +const RowActionsDropdown: React.FC = ({ actions, className = '' }) => { + const [isOpen, setIsOpen] = useState(false); + const [position, setPosition] = useState({ top: 0, left: 0 }); + const dropdownRef = useRef(null); + const buttonRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const handleToggle = (e: React.MouseEvent) => { + e.stopPropagation(); + + if (!isOpen && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect(); + const isMobile = window.innerWidth < 768; + const dropdownWidth = isMobile ? 140 : 150; + const dropdownHeight = actions.length * (isMobile ? 36 : 40); // تخمین ارتفاع منو + + let left = rect.left + window.scrollX - dropdownWidth + rect.width; + let top = rect.bottom + window.scrollY; + + // بررسی اینکه منو از سمت راست خارج نشود + if (left + dropdownWidth > window.innerWidth) { + left = rect.left + window.scrollX - dropdownWidth; + } + + // بررسی اینکه منو از سمت چپ خارج نشود + if (left < 0) { + left = isMobile ? 20 : 40; + } + + // بررسی اینکه منو از پایین خارج نشود + if (top + dropdownHeight > window.innerHeight + window.scrollY) { + top = rect.top + window.scrollY - dropdownHeight; + } + + setPosition({ top, left }); + } + + setIsOpen(!isOpen); + }; + + const handleActionClick = (action: RowActionItem) => { + action.onClick(); + setIsOpen(false); + }; + + const renderDropdown = () => { + if (!isOpen) return null; + + return createPortal( +
+ {actions.map((action, index) => ( + + ))} +
, + document.body + ); + }; + + return ( +
+ + + {renderDropdown()} +
+ ); +}; + +export default RowActionsDropdown; \ No newline at end of file diff --git a/src/components/Table.tsx b/src/components/Table.tsx index d0d9738..5370c02 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -3,6 +3,8 @@ import DefaultTableSkeleton from './DefaultTableSkeleton'; import Td from './Td'; import { TableProps, RowDataType, ColumnType } from './types/TableTypes'; import { Checkbox } from './ui/checkbox'; +import RowActionsDropdown from './RowActionsDropdown'; +import Pagination from './Pagination'; const Table = ({ columns, @@ -18,6 +20,10 @@ const Table = ({ actions = null, actionsPosition = 'top', selectable = false, + selectedActions = null, + onSelectionChange, + rowActions, + pagination, }: TableProps): React.ReactElement => { const [selectedRows, setSelectedRows] = useState([]); @@ -31,36 +37,42 @@ const Table = ({ const getCellClassName = (column: ColumnType): string => { const alignClass = column.align ? `text-${column.align}` : ''; - return `p-3 ${alignClass} ${column.className || ''}`.trim(); + return `px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${alignClass} ${column.className || ''}`.trim(); }; const handleRowClick = (item: T) => { - if (selectable) { - if (selectedRows.includes(item)) { - setSelectedRows(selectedRows.filter((row) => row !== item)); - } else { - setSelectedRows([...selectedRows, item]); - } - } - if (onRowClick) { onRowClick(item.id, item); } }; const handleSelectAll = () => { - if (selectedRows.length === data.length) { - setSelectedRows([]); - } else { - setSelectedRows([...data]); + const newSelectedRows = selectedRows.length === data.length ? [] : [...data]; + setSelectedRows(newSelectedRows); + if (onSelectionChange) { + onSelectionChange(newSelectedRows); } }; + const handleSingleRowSelect = (item: T, checked: boolean) => { + const newSelectedRows = checked + ? [...selectedRows, item] + : selectedRows.filter((row) => row.id !== item.id); + setSelectedRows(newSelectedRows); + if (onSelectionChange) { + onSelectionChange(newSelectedRows); + } + }; + + const isRowSelected = (item: T): boolean => { + return selectedRows.some((row) => row.id === item.id); + }; + const renderTableBody = () => { if (isLoading) { return ( - + ); } @@ -68,7 +80,7 @@ const Table = ({ if (data.length === 0) { return ( - + {noDataMessage} @@ -77,28 +89,31 @@ const Table = ({ return data.map((item, rowIndex) => ( handleRowClick(item)} > {selectable && ( - + { - if (checked) { - setSelectedRows([...selectedRows, item]); - } else { - setSelectedRows(selectedRows.filter((row) => row !== item)); - } - }} + checked={isRowSelected(item)} + onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)} /> )} {columns.map((col) => ( - + {col.render ? col.render(item) : item[col.key] as React.ReactNode} ))} + {rowActions && ( + + + + )} )); }; @@ -107,12 +122,10 @@ const Table = ({ if (!showHeader || actionsPosition === 'header-replace') return null; return ( - + {selectable && ( - + 0 && selectedRows.length === data.length} onCheckedChange={handleSelectAll} @@ -128,41 +141,97 @@ const Table = ({ ))} + {rowActions && ( + + )} ); }; const renderActions = () => { - if (!actions) return null; + if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null; return ( -
- {actions} +
+
+ {actions} + {selectable && selectedRows.length > 0 && selectedActions && selectedActions} +
); }; return ( -
+
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()} - - {renderHeader()} - {actionsPosition === 'header-replace' && ( - - - - - - )} - - {renderTableBody()} - -
- {renderActions()} -
+ +
+ + {renderHeader()} + {actionsPosition === 'header-replace' && ( + + + + + + )} + + {renderTableBody()} + +
+ {renderActions()} +
+
+ + {pagination && ( + + )}
); }; -export default Table; \ No newline at end of file +export default Table; + +/* +مثال استفاده با pagination: + +import Table from '@/components/Table'; +import { useState } from 'react'; + +const MyComponent = () => { + const [currentPage, setCurrentPage] = useState(1); + const totalPages = 10; + + const columns = [ + { title: 'نام', key: 'name' }, + { title: 'ایمیل', key: 'email' }, + ]; + + const data = [ + { id: 1, name: 'علی', email: 'ali@example.com' }, + { id: 2, name: 'فاطمه', email: 'fateme@example.com' }, + ]; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + // اینجا می‌توانید API call جدید برای دریافت داده‌های صفحه جدید بزنید + }; + + return ( + + ); +}; +*/ \ No newline at end of file diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 4385905..062fb3d 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -24,19 +24,19 @@ const Tabs: FC = (props: Props) => { return ( + spaceBetween={20} + className='px-4 md:px-8 xl:px-10 max-w-full w-fit items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-4 md:gap-6 xl:gap-10 flex h-[60px] md:h-[70px] xl:h-[80px] rounded-[20px] md:rounded-[28px] xl:rounded-[32px] bg-white bg-opacity-45'> { props.items.map((item: Item, index: number) => { return ( props.onChange(item.value)} key={item.value} className={clx( - 'flex flex-col max-w-fit mt-[15px] items-center gap-2 cursor-pointer', - index === 0 && 'pr-[30px]', + 'flex flex-col max-w-fit mt-[10px] md:mt-[12px] xl:mt-[15px] items-center gap-1 md:gap-2 cursor-pointer', + index === 0 && 'pr-[15px] md:pr-[20px] xl:pr-[30px]', index === props.items.length - 1 && props.items.length > 3 && 'pl-[5px]', props.active === item.value && 'text-black' )}> {item.icon} -
+
{item.label}
diff --git a/src/components/Td.tsx b/src/components/Td.tsx index d581f8a..14314ed 100644 --- a/src/components/Td.tsx +++ b/src/components/Td.tsx @@ -8,7 +8,7 @@ interface Props { const Td: FC = (props: Props) => { return ( -
+ { props.text ? props.text diff --git a/src/components/newMessage/NewMessage.tsx b/src/components/newMessage/NewMessage.tsx index 07c496d..71a74ad 100644 --- a/src/components/newMessage/NewMessage.tsx +++ b/src/components/newMessage/NewMessage.tsx @@ -17,59 +17,60 @@ const NewMessage: FC = () => { {openNewMessage && ( <>
setOpenNewMessage(false)} - // style={{ left: '250px' }} /> -
+
{/* Header */} -
-
+
+
{t('new_message.title')}
-
setOpenNewMessage(false)}> - +
setOpenNewMessage(false)} className='cursor-pointer p-1'> +
{/* Content */} - -
+
-
- + +
+
-
- -
- -
+
- -
diff --git a/src/components/types/TableTypes.ts b/src/components/types/TableTypes.ts index f0c15f3..42566c2 100644 --- a/src/components/types/TableTypes.ts +++ b/src/components/types/TableTypes.ts @@ -1,4 +1,5 @@ import { ReactNode } from "react"; +import { RowActionItem } from "../RowActionsDropdown"; export type RowDataType = Record & { id: string | number }; @@ -12,6 +13,12 @@ export interface ColumnType { className?: string; } +export interface PaginationConfig { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + export interface TableProps { columns: ColumnType[]; data: T[]; @@ -27,4 +34,8 @@ export interface TableProps { actionsClassName?: string; actionsPosition?: "top" | "header-replace" | "above-header"; selectable?: boolean; + selectedActions?: ReactNode; + onSelectionChange?: (selectedRows: T[]) => void; + rowActions?: (item: T) => RowActionItem[]; + pagination?: PaginationConfig; } diff --git a/src/index.css b/src/index.css index d796b03..200774a 100644 --- a/src/index.css +++ b/src/index.css @@ -22,6 +22,17 @@ body { font-feature-settings: "lnum"; /* اطمینان از نمایش اعداد انگلیسی */ } +/* ریسپانسیو شدن برای صفحات کوچک */ +@media (max-width: 768px) { + .no-scrollbar::-webkit-scrollbar { + display: none; + } + .no-scrollbar { + -ms-overflow-style: none; + scrollbar-width: none; + } +} + html, body { height: 100%; @@ -52,19 +63,6 @@ textarea::placeholder { --radius-2.5: 10px; } -td { - @apply px-6 py-4 whitespace-nowrap text-xs; -} -.thead { - @apply h-[69px] bg-white text-sm text-[#8C90A3] rounded-3xl overflow-hidden; -} -.tr { - @apply w-full h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-[#F4F5F9]; -} -.action { - @apply h-[74px] bg-white flex items-center px-6; -} - .rmdp-input { min-height: 40px; background-color: white; diff --git a/src/langs/fa.json b/src/langs/fa.json index cce41ae..60532a2 100644 --- a/src/langs/fa.json +++ b/src/langs/fa.json @@ -22,6 +22,8 @@ "from_date": "از تاریخ", "to_date": "تا تاریخ", "all": "همه", + "read": "خوانده شده", + "unread": "خوانده نشده", "search": "جستجو" }, "sent": { @@ -29,6 +31,32 @@ "from_date": "از تاریخ", "to_date": "تا تاریخ", "all": "همه", + "read": "خوانده شده", + "unread": "خوانده نشده", + "search": "جستجو" + }, + "draft": { + "title": "پیش نویس ها", + "from_date": "از تاریخ", + "to_date": "تا تاریخ", + "search": "جستجو" + }, + "archive": { + "title": "آرشیو شده ها", + "from_date": "از تاریخ", + "to_date": "تا تاریخ", + "all": "همه", + "read": "خوانده شده", + "unread": "خوانده نشده", + "search": "جستجو" + }, + "trash": { + "title": "سطل زباله", + "from_date": "از تاریخ", + "to_date": "تا تاریخ", + "all": "همه", + "read": "خوانده شده", + "unread": "خوانده نشده", "search": "جستجو" }, "setting": { @@ -48,8 +76,5 @@ "recipients_placeholder": "نام گیرنده", "subject_placeholder": "موضوع پیام", "message_placeholder": "متن پیام" - }, - "draft": { - "title": "پیش نویس ها" } } diff --git a/src/pages/Trash/List.tsx b/src/pages/Trash/List.tsx index f7da255..dcde8fb 100644 --- a/src/pages/Trash/List.tsx +++ b/src/pages/Trash/List.tsx @@ -1,26 +1,28 @@ import Filters, { FilterValues } from '../../components/Filters'; -import { FC, ReactNode, useState } from 'react' +import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; -import { InfoCircle, More, Refresh2 } from 'iconsax-react'; +import { InfoCircle, More, Refresh2, Trash, RecoveryConvert } from 'iconsax-react'; +import { RowActionItem } from '../../components/RowActionsDropdown'; interface Message extends Record { id: string; - title: string | ReactNode; + title: string; sender: string; date: string; read: boolean; } -const TrashList: FC = () => { +const List: FC = () => { const { t } = useTranslation(); const [isLoading] = useState(false); + const [selectedMessages, setSelectedMessages] = useState([]); const messageData: Message[] = [ - { id: '1', title:
پیش نویس
, sender: 'علی محمدی', date: '1402/05/12', read: true }, - { id: '2', title:
ارسالی
, sender: 'زهرا احمدی', date: '1402/05/13', read: true }, - { id: '3', title:
دریافت شده
, sender: 'مدیریت', date: '1402/05/14', read: true }, + { id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, + { id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true }, + { id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true }, ]; const columns: ColumnType[] = [ @@ -29,49 +31,94 @@ const TrashList: FC = () => { title: 'عنوان پیام', render: (item) => (
- {!item.read &&
} - {item.title} + {!item.read &&
} + {item.title}
) }, - { key: 'status', title: 'وضعیت' }, - { key: 'sender', title: 'فرستنده' }, - { key: 'date', title: 'تاریخ', align: 'center' }, + { + key: 'sender', + title: 'فرستنده', + render: (item) => ( + {item.sender} + ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {item.date} + ) + }, ]; const tableActions = ( -
- - +
+ +
); + const selectedActions = ( + <> + handleRestoreSelected()} className="cursor-pointer" /> + handleDeleteSelected()} className="cursor-pointer" /> + + ); + const handleFilterChange = (newFilters: FilterValues) => { console.log('Applied filters:', newFilters); }; + const handleSelectionChange = (selectedRows: Message[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + console.log('حذف دائم پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق حذف دائم را پیاده‌سازی کنید + }; + + const handleRestoreSelected = () => { + console.log('بازگردانی پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق بازگردانی را پیاده‌سازی کنید + }; + + const getRowActions = (message: Message): RowActionItem[] => [ + { + label: 'بازگردانی', + icon: , + onClick: () => console.log('بازگردانی پیام', message.id), + }, + { + label: 'حذف دائم', + icon: , + onClick: () => console.log('حذف دائم پیام', message.id), + }, + ]; + return ( -
-

{t('draft.title')}

+
+

{t('trash.title')}

@@ -81,18 +128,20 @@ const TrashList: FC = () => { isLoading={isLoading} showHeader={false} actions={tableActions} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} noDataMessage={
- -
هیچ پیامی یافت نشد
+ +
سطل آشغال خالی است
} selectable={true} + rowActions={getRowActions} /> -
) } -export default TrashList +export default List diff --git a/src/pages/archive/List.tsx b/src/pages/archive/List.tsx index 595a6ed..fd3c55c 100644 --- a/src/pages/archive/List.tsx +++ b/src/pages/archive/List.tsx @@ -1,27 +1,28 @@ import Filters, { FilterValues } from '../../components/Filters'; -import { FC, ReactNode, useState } from 'react' +import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; -import { InfoCircle, More, Refresh2 } from 'iconsax-react'; +import { InfoCircle, More, Refresh2, Trash, ArchiveSlash } from 'iconsax-react'; +import { RowActionItem } from '../../components/RowActionsDropdown'; interface Message extends Record { id: string; - title: string | ReactNode; - status: ReactNode; + title: string; sender: string; date: string; read: boolean; } -const ArchiveList: FC = () => { +const List: FC = () => { const { t } = useTranslation(); const [isLoading] = useState(false); + const [selectedMessages, setSelectedMessages] = useState([]); const messageData: Message[] = [ - { id: '1', title:
پیش نویس
, status:
آرشیو شده
, sender: 'علی محمدی', date: '1402/05/12', read: true }, - { id: '2', title:
ارسالی
, status:
آرشیو شده
, sender: 'زهرا احمدی', date: '1402/05/13', read: true }, - { id: '3', title:
دریافت شده
, status:
آرشیو شده
, sender: 'مدیریت', date: '1402/05/14', read: true }, + { id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, + { id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true }, + { id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true }, ]; const columns: ColumnType[] = [ @@ -30,49 +31,94 @@ const ArchiveList: FC = () => { title: 'عنوان پیام', render: (item) => (
- {!item.read &&
} - {item.title} + {!item.read &&
} + {item.title}
) }, - { key: 'status', title: 'وضعیت' }, - { key: 'sender', title: 'فرستنده' }, - { key: 'date', title: 'تاریخ', align: 'center' }, + { + key: 'sender', + title: 'فرستنده', + render: (item) => ( + {item.sender} + ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {item.date} + ) + }, ]; const tableActions = ( -
- - +
+ +
); + const selectedActions = ( + <> + handleUnarchiveSelected()} className="cursor-pointer" /> + handleDeleteSelected()} className="cursor-pointer" /> + + ); + const handleFilterChange = (newFilters: FilterValues) => { console.log('Applied filters:', newFilters); }; + const handleSelectionChange = (selectedRows: Message[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق حذف را پیاده‌سازی کنید + }; + + const handleUnarchiveSelected = () => { + console.log('خروج از آرشیو پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق خروج از آرشیو را پیاده‌سازی کنید + }; + + const getRowActions = (message: Message): RowActionItem[] => [ + { + label: 'خروج از آرشیو', + icon: , + onClick: () => console.log('خروج از آرشیو', message.id), + }, + { + label: 'حذف', + icon: , + onClick: () => console.log('حذف پیام', message.id), + }, + ]; + return ( -
-

{t('draft.title')}

+
+

{t('archive.title')}

@@ -82,18 +128,20 @@ const ArchiveList: FC = () => { isLoading={isLoading} showHeader={false} actions={tableActions} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} noDataMessage={
- -
هیچ پیامی یافت نشد
+ +
هیچ پیام آرشیو شده‌ای یافت نشد
} selectable={true} + rowActions={getRowActions} /> -
) } -export default ArchiveList +export default List diff --git a/src/pages/draft/List.tsx b/src/pages/draft/List.tsx index 64368d2..ac0da61 100644 --- a/src/pages/draft/List.tsx +++ b/src/pages/draft/List.tsx @@ -1,13 +1,14 @@ import Filters, { FilterValues } from '../../components/Filters'; -import { FC, ReactNode, useState } from 'react' +import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; -import { InfoCircle, More, Refresh2 } from 'iconsax-react'; +import { InfoCircle, More, Refresh2, Trash, Edit } from 'iconsax-react'; +import { RowActionItem } from '../../components/RowActionsDropdown'; interface Message extends Record { id: string; - title: string | ReactNode; + title: string; sender: string; date: string; read: boolean; @@ -16,11 +17,12 @@ interface Message extends Record { const List: FC = () => { const { t } = useTranslation(); const [isLoading] = useState(false); + const [selectedMessages, setSelectedMessages] = useState([]); const messageData: Message[] = [ - { id: '1', title:
پیش نویس
, sender: 'علی محمدی', date: '1402/05/12', read: true }, - { id: '2', title:
پیش نویس
, sender: 'زهرا احمدی', date: '1402/05/13', read: true }, - { id: '3', title:
پیش نویس
, sender: 'مدیریت', date: '1402/05/14', read: true }, + { id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, + { id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true }, + { id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true }, ]; const columns: ColumnType[] = [ @@ -29,48 +31,84 @@ const List: FC = () => { title: 'عنوان پیام', render: (item) => (
- {!item.read &&
} - {item.title} + {!item.read &&
} + {item.title}
) }, - { key: 'sender', title: 'فرستنده' }, - { key: 'date', title: 'تاریخ', align: 'center' }, + { + key: 'sender', + title: 'فرستنده', + render: (item) => ( + {item.sender} + ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {item.date} + ) + }, ]; const tableActions = ( -
- - +
+ +
); + const selectedActions = ( + <> + handleDeleteSelected()} className="cursor-pointer" /> + handleEditSelected()} className="cursor-pointer" /> + + ); + const handleFilterChange = (newFilters: FilterValues) => { console.log('Applied filters:', newFilters); }; + const handleSelectionChange = (selectedRows: Message[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق حذف را پیاده‌سازی کنید + }; + + const handleEditSelected = () => { + console.log('ویرایش پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق ویرایش را پیاده‌سازی کنید + }; + + const getRowActions = (message: Message): RowActionItem[] => [ + { + label: 'ویرایش', + icon: , + onClick: () => console.log('ویرایش پیام', message.id), + }, + { + label: 'حذف', + icon: , + onClick: () => console.log('حذف پیام', message.id), + }, + ]; + return ( -
-

{t('draft.title')}

+
+

{t('draft.title')}

@@ -80,16 +118,18 @@ const List: FC = () => { isLoading={isLoading} showHeader={false} actions={tableActions} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} noDataMessage={
- -
هیچ پیامی یافت نشد
+ +
هیچ پیش‌نویسی یافت نشد
} selectable={true} + rowActions={getRowActions} /> -
) } diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index 60a053a..14f7917 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -2,7 +2,34 @@ import { FC } from 'react' const Home: FC = () => { return ( -
home page
+
+
+

+ خوش آمدید به سیستم مدیریت ایمیل +

+

+ از منوی کناری می‌توانید به بخش‌های مختلف سیستم دسترسی داشته باشید +

+
+
+

پیام‌های دریافتی

+

مدیریت ایمیل‌های واردشده

+
+
+

پیام‌های ارسالی

+

مشاهده ایمیل‌های ارسال شده

+
+
+

پیش‌نویس‌ها

+

ادامه نوشتن ایمیل‌های ناتمام

+
+
+

تنظیمات

+

پیکربندی سیستم و حساب کاربری

+
+
+
+
) } diff --git a/src/pages/received/List.tsx b/src/pages/received/List.tsx index 8d3afdc..2236025 100644 --- a/src/pages/received/List.tsx +++ b/src/pages/received/List.tsx @@ -3,7 +3,8 @@ import { FC, useState } from 'react' import { useTranslation } from 'react-i18next' import Table from '../../components/Table'; import { ColumnType } from '../../components/types/TableTypes'; -import { InfoCircle, More, Refresh2 } from 'iconsax-react'; +import { InfoCircle, More, Refresh2, Trash, Edit, Archive, ArchiveSlash } from 'iconsax-react'; +import { RowActionItem } from '../../components/RowActionsDropdown'; interface Message extends Record { id: string; @@ -16,6 +17,7 @@ interface Message extends Record { const List: FC = () => { const { t } = useTranslation(); const [isLoading] = useState(false); + const [selectedMessages, setSelectedMessages] = useState([]); const messageData: Message[] = [ { id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, @@ -29,29 +31,87 @@ const List: FC = () => { title: 'عنوان پیام', render: (item) => (
- {!item.read &&
} - {item.title} + {!item.read &&
} + {item.title}
) }, - { key: 'sender', title: 'فرستنده' }, - { key: 'date', title: 'تاریخ', align: 'center' }, + { + key: 'sender', + title: 'فرستنده', + render: (item) => ( + {item.sender} + ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {item.date} + ) + }, ]; const tableActions = ( -
- - +
+ +
); + const selectedActions = ( + <> + handleDeleteSelected()} className="cursor-pointer" /> + handleArchiveSelected()} className="cursor-pointer" /> + handleMarkAsReadSelected()} className="cursor-pointer" /> + + ); + const handleFilterChange = (newFilters: FilterValues) => { console.log('Applied filters:', newFilters); }; + const handleSelectionChange = (selectedRows: Message[]) => { + setSelectedMessages(selectedRows); + }; + + const handleDeleteSelected = () => { + console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق حذف را پیاده‌سازی کنید + }; + + const handleArchiveSelected = () => { + console.log('بایگانی پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); + // اینجا منطق بایگانی را پیاده‌سازی کنید + }; + + const handleMarkAsReadSelected = () => { + console.log('علامت‌گذاری به عنوان خوانده شده:', selectedMessages.map(m => m.id)); + // اینجا منطق علامت‌گذاری را پیاده‌سازی کنید + }; + + const getRowActions = (message: Message): RowActionItem[] => [ + { + label: 'خروج از آرشیو', + icon: , + onClick: () => console.log('خروج از آرشیو', message.id), + }, + { + label: 'خوانده شده', + icon: , + onClick: () => console.log('علامت‌گذاری به عنوان خوانده شده', message.id), + }, + { + label: 'حذف', + icon: , + onClick: () => console.log('حذف پیام', message.id), + }, + ]; + return ( -
-

{t('received.title')}

+
+

{t('received.title')}

{ { type: 'input', name: 'search', placeholder: t('received.search') } ]} onChange={handleFilterChange} - className="mt-8 flex justify-between items-center" searchField="search" /> @@ -80,14 +139,22 @@ const List: FC = () => { isLoading={isLoading} showHeader={false} actions={tableActions} + selectedActions={selectedActions} + onSelectionChange={handleSelectionChange} onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} noDataMessage={
- -
هیچ پیامی یافت نشد
+ +
هیچ پیامی یافت نشد
} selectable={true} + rowActions={getRowActions} + pagination={{ + totalPages: 10, + currentPage: 1, + onPageChange: () => { } + }} />
diff --git a/src/pages/sent/List.tsx b/src/pages/sent/List.tsx index ddb5aeb..2e383ec 100644 --- a/src/pages/sent/List.tsx +++ b/src/pages/sent/List.tsx @@ -29,19 +29,32 @@ const List: FC = () => { title: 'عنوان پیام', render: (item) => (
- {!item.read &&
} - {item.title} + {!item.read &&
} + {item.title}
) }, - { key: 'sender', title: 'فرستنده' }, - { key: 'date', title: 'تاریخ', align: 'center' }, + { + key: 'sender', + title: 'فرستنده', + render: (item) => ( + {item.sender} + ) + }, + { + key: 'date', + title: 'تاریخ', + align: 'center', + render: (item) => ( + {item.date} + ) + }, ]; const tableActions = ( -
- - +
+ +
); @@ -50,8 +63,8 @@ const List: FC = () => { }; return ( -
-

{t('sent.title')}

+
+

{t('sent.title')}

{ { type: 'input', name: 'search', placeholder: t('sent.search') } ]} onChange={handleFilterChange} - className="mt-8 flex justify-between items-center" searchField="search" /> @@ -83,8 +95,8 @@ const List: FC = () => { onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} noDataMessage={
- -
هیچ پیامی یافت نشد
+ +
هیچ پیامی یافت نشد
} selectable={true} diff --git a/src/pages/setting/Setting.tsx b/src/pages/setting/Setting.tsx index d15fea2..4f0cdb0 100644 --- a/src/pages/setting/Setting.tsx +++ b/src/pages/setting/Setting.tsx @@ -23,9 +23,13 @@ const Setting: FC = () => { return
; case SettingTabEnum.SETTING_PERSONALITY: return ( -
- - +
+
+ +
+
+ +
); default: @@ -34,29 +38,29 @@ const Setting: FC = () => { }; return ( -
-

{t('setting.title')}

+
+

{t('setting.title')}

-
+
, + icon: , label: t('setting.mail_server'), value: SettingTabEnum.SETTING_MAIL_SERVER }, { - icon: , + icon: , label: t('setting.domain'), value: SettingTabEnum.SETTING_DOMAIN }, { - icon: , + icon: , label: t('setting.address'), value: SettingTabEnum.SETTING_ADDRESS }, { - icon: , + icon: , label: t('setting.personality'), value: SettingTabEnum.SETTING_PERSONALITY } diff --git a/src/shared/Header.tsx b/src/shared/Header.tsx index bf29e06..a10310a 100644 --- a/src/shared/Header.tsx +++ b/src/shared/Header.tsx @@ -27,9 +27,9 @@ const Header: FC = () => { }, [location.pathname]); return ( -
+
-
+
{
{/* */} -
+
- + - +
diff --git a/src/shared/Main.tsx b/src/shared/Main.tsx index 2ee9d2f..ec7cc92 100644 --- a/src/shared/Main.tsx +++ b/src/shared/Main.tsx @@ -5,12 +5,12 @@ import Header from './Header' const Main: FC = () => { return ( -
+
-
-
-
+
+
+
diff --git a/src/shared/SideBar.tsx b/src/shared/SideBar.tsx index 36dcf1a..41ebf81 100644 --- a/src/shared/SideBar.tsx +++ b/src/shared/SideBar.tsx @@ -23,32 +23,35 @@ const SideBar: FC = () => { return ( <> { - openSidebar &&
setOpenSidebar(false)} /> + openSidebar &&
setOpenSidebar(false)} /> }
-
- +
+
-
+
-
+
{t('sidebar.menu')}
@@ -89,7 +92,7 @@ const SideBar: FC = () => { />
-
+
{t('sidebar.other')}
@@ -109,7 +112,7 @@ const SideBar: FC = () => { />
-
+
} diff --git a/src/shared/SideBarItem.tsx b/src/shared/SideBarItem.tsx index 49dab92..1301e2d 100644 --- a/src/shared/SideBarItem.tsx +++ b/src/shared/SideBarItem.tsx @@ -21,17 +21,20 @@ const SideBarItem: FC = (props: Props) => { } return ( - + { !props.isWithoutLine &&
} -
+
{props.icon} -
+
{props.title}