reponsive and selective actions and row actioin + pagination

This commit is contained in:
hamid zarghami
2025-06-17 20:52:40 +03:30
parent 71fd53c810
commit 4c869c42c6
24 changed files with 869 additions and 287 deletions
+27 -27
View File
@@ -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<HTMLButtonElement> &
XOR<{ children: ReactNode }, { label: string }>;
loading?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
}
const Button: FC<Props> = memo((props: Props) => {
const Button: FC<ButtonProps> = (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 (
<button {...props} className={`${buttonClass} ${props.className}`} >
{
props.isLoading ?
<div className='flex gap-2 items-center'>
{
!props.percentage ?
<MoonLoader size={20} color='#fff' />
:
<span>{props.percentage}%</span>
}
</div>
:
props.label || props.children
}
<button
onClick={props.onClick}
type={props.type || 'button'}
disabled={props.disabled || props.loading}
className={buttonClass}
>
{props.loading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-3 h-3 md:w-4 md:h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
درحال بارگذاری...
</div>
) : (
props.children || props.label
)}
</button>
)
})
}
export default Button
+1 -1
View File
@@ -12,7 +12,7 @@ const DefaultTableSkeleton: FC<Props> = ({ tdCount, trCount = 5 }) => {
<Fragment>
{
Array.from({ length: trCount }).map((_, rowIndex) => (
<tr className="tr" key={rowIndex}>
<tr className="w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5]" key={rowIndex}>
{
Array.from({ length: tdCount }).map((_, colIndex) => (
<Td text={''} key={colIndex}>
+4 -4
View File
@@ -44,8 +44,8 @@ const Filters: FC<FiltersProps> = ({
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<FilterValues>({});
@@ -111,7 +111,7 @@ const Filters: FC<FiltersProps> = ({
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<FiltersProps> = ({
{otherFields.map(renderField)}
</div>
{searchFieldObj && (
<div>
<div className="w-full md:w-auto">
{renderField(searchFieldObj)}
</div>
)}
-1
View File
@@ -96,7 +96,6 @@ const Input: FC<Props> = (props: Props) => {
{
props.type === 'password' &&
<Eye onClick={() => setShowPassword((oldValue) => !oldValue)} size={20} color='#8C90A3' className='absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
// <img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
}
{
+109
View File
@@ -0,0 +1,109 @@
import React from "react";
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
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 (
<div className="flex justify-center items-center gap-1 md:gap-2 py-4 md:py-6 mt-3 md:mt-4 border-t border-gray-100">
{/* دکمه صفحه قبل */}
<button
className={`px-2 md:px-3 py-2 text-xs md:text-sm rounded-lg transition-colors ${currentPage === 1
? "text-gray-400 cursor-not-allowed"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
disabled={currentPage === 1}
>
قبلی
</button>
{/* شماره صفحات */}
<div className="flex gap-1">
{pageNumbers.map((page, index) =>
typeof page === "number" ? (
<button
key={index}
className={`w-8 h-8 md:w-10 md:h-10 text-xs md:text-sm rounded-lg transition-colors ${currentPage === page
? "bg-primary text-white shadow-sm"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => onPageChange(page)}
>
{page}
</button>
) : (
<span key={index} className="flex items-center px-1 md:px-2 text-gray-400 text-xs md:text-sm">
{page}
</span>
)
)}
</div>
{/* دکمه صفحه بعد */}
<button
className={`px-2 md:px-3 py-2 text-xs md:text-sm rounded-lg transition-colors ${currentPage === totalPages
? "text-gray-400 cursor-not-allowed"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
بعدی
</button>
</div>
);
};
export default Pagination;
+117
View File
@@ -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<RowActionsDropdownProps> = ({ actions, className = '' }) => {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(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(
<div
ref={dropdownRef}
className="fixed py-1 md:py-2 bg-white rounded-xl md:rounded-2xl shadow-lg z-[9999] min-w-[140px] md:min-w-[150px]"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
}}
>
{actions.map((action, index) => (
<button
key={index}
onClick={() => handleActionClick(action)}
className={`w-full text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 ${index === 0 ? 'rounded-t-lg' : ''
} ${action.className || ''}`}
>
{action.icon && <span className="ml-2">{action.icon}</span>}
{action.label}
</button>
))}
</div>,
document.body
);
};
return (
<div className={className}>
<button
ref={buttonRef}
onClick={handleToggle}
className="p-1 hover:bg-gray-100 rounded-full transition-colors"
>
<More size={14} color="black" className="rotate-90" />
</button>
{renderDropdown()}
</div>
);
};
export default RowActionsDropdown;
+119 -50
View File
@@ -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 = <T extends RowDataType>({
columns,
@@ -18,6 +20,10 @@ const Table = <T extends RowDataType>({
actions = null,
actionsPosition = 'top',
selectable = false,
selectedActions = null,
onSelectionChange,
rowActions,
pagination,
}: TableProps<T>): React.ReactElement => {
const [selectedRows, setSelectedRows] = useState<T[]>([]);
@@ -31,36 +37,42 @@ const Table = <T extends RowDataType>({
const getCellClassName = (column: ColumnType<T>): 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 (
<Fragment>
<DefaultTableSkeleton tdCount={columns.length} trCount={emptyRowsCount} />
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
</Fragment>
);
}
@@ -68,7 +80,7 @@ const Table = <T extends RowDataType>({
if (data.length === 0) {
return (
<tr>
<td colSpan={columns.length} className="p-8 text-center text-gray-500">
<td colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 py-6 md:py-8 text-center text-gray-500">
{noDataMessage}
</td>
</tr>
@@ -77,28 +89,31 @@ const Table = <T extends RowDataType>({
return data.map((item, rowIndex) => (
<tr
key={item.id}
className={`tr border-b hover:bg-gray-50 ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
className={`w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<td className="p-3 w-10 relative z-[5]">
<td className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]">
<Checkbox
checked={selectedRows.includes(item)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedRows([...selectedRows, item]);
} else {
setSelectedRows(selectedRows.filter((row) => row !== item));
}
}}
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</td>
)}
{columns.map((col) => (
<td key={`${item.id}-${col.key}`} className={getCellClassName(col)} style={{ width: col.width }}>
<td
key={`${item.id}-${col.key}`}
className={getCellClassName(col)}
style={{ width: col.width }}
>
{col.render ? col.render(item) : item[col.key] as React.ReactNode}
</td>
))}
{rowActions && (
<td className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]">
<RowActionsDropdown actions={rowActions(item)} />
</td>
)}
</tr>
));
};
@@ -107,12 +122,10 @@ const Table = <T extends RowDataType>({
if (!showHeader || actionsPosition === 'header-replace') return null;
return (
<thead className={`thead ${headerClassName}`}>
<thead className={`h-[60px] md:h-[69px] bg-white text-sm text-[#8C90A3] ${headerClassName}`}>
<tr>
{selectable && (
<th
className="p-3 w-10 relative z-[5]"
>
<th className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]">
<Checkbox
checked={data.length > 0 && selectedRows.length === data.length}
onCheckedChange={handleSelectAll}
@@ -128,41 +141,97 @@ const Table = <T extends RowDataType>({
<Td text={col.title} />
</th>
))}
{rowActions && (
<th className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10"></th>
)}
</tr>
</thead>
);
};
const renderActions = () => {
if (!actions) return null;
if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null;
return (
<div className={'action'}>
{actions}
<div className="h-[64px] rounded-t-2xl md:rounded-t-3xl md:h-[74px] bg-white flex items-center px-3 md:px-6">
<div className='flex items-center gap-2 md:gap-4'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
</div>
</div>
);
};
return (
<div className={`relative overflow-x-auto rounded-3xl mt-9 w-full ${className}`}>
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
<table className="w-full text-sm border-collapse">
{renderHeader()}
{actionsPosition === 'header-replace' && (
<thead>
<tr>
<th colSpan={columns.length} className="p-0">
{renderActions()}
</th>
</tr>
</thead>
)}
<tbody>
{renderTableBody()}
</tbody>
</table>
<div className="overflow-x-auto overflow-hidden rounded-b-2xl md:rounded-b-3xl bg-white shadow-sm">
<table className="w-full text-sm border-collapse min-w-[600px]">
{renderHeader()}
{actionsPosition === 'header-replace' && (
<thead>
<tr>
<th colSpan={columns.length} className="p-0">
{renderActions()}
</th>
</tr>
</thead>
)}
<tbody>
{renderTableBody()}
</tbody>
</table>
</div>
{pagination && (
<Pagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={pagination.onPageChange}
/>
)}
</div>
);
};
export default Table;
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 (
<Table
columns={columns}
data={data}
pagination={{
currentPage,
totalPages,
onPageChange: handlePageChange
}}
/>
);
};
*/
+5 -5
View File
@@ -24,19 +24,19 @@ const Tabs: FC<Props> = (props: Props) => {
return (
<Swiper
slidesPerView='auto'
spaceBetween={30}
className='px-10 max-w-full w-fit items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-10 flex h-[80px] rounded-[32px] bg-white bg-opacity-45'>
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 (
<SwiperSlide style={SWIPER_SLIDE_STYLE} onClick={() => 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}
<div className='text-xs'>
<div className='text-[10px] md:text-xs'>
{item.label}
</div>
</SwiperSlide>
+1 -1
View File
@@ -8,7 +8,7 @@ interface Props {
const Td: FC<Props> = (props: Props) => {
return (
<td className='td' style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
<td className='px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs' style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
{
props.text ?
props.text
+31 -30
View File
@@ -17,59 +17,60 @@ const NewMessage: FC = () => {
{openNewMessage && (
<>
<div
className='fixed inset-0 bg-opacity-50 z-[9998]'
className='fixed inset-0 bg-black/50 z-[9998]'
onClick={() => setOpenNewMessage(false)}
// style={{ left: '250px' }}
/>
<div className={`fixed left-8 bottom-4 bg-white rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] transition-all duration-300 p-8 w-[800px] `}>
<div className='fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 md:top-4 xl:left-8 xl:right-auto xl:bottom-4 xl:top-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'>
{/* Header */}
<div className='flex justify-between items-center'>
<div className=''>
<div className='flex justify-between items-center mb-6 md:mb-8'>
<div className='text-lg md:text-xl xl:text-2xl font-semibold'>
{t('new_message.title')}
</div>
<div onClick={() => setOpenNewMessage(false)}>
<CloseCircle size={24} color='#292D32' />
<div onClick={() => setOpenNewMessage(false)} className='cursor-pointer p-1'>
<CloseCircle size={20} className='md:w-6 md:h-6' color='#292D32' />
</div>
</div>
{/* Content */}
<div className='mt-10'>
<div className='space-y-6 md:space-y-8'>
<Input
label={t('new_message.to')}
/>
<div className='mt-8'>
<Input
label={t('new_message.subject')}
<Input
label={t('new_message.subject')}
/>
<div>
<ReactQuill
modules={{
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ align: [] }],
['clean']
]
}}
theme="snow"
value={value}
onChange={setValue}
style={{ minHeight: '120px' }}
className='text-sm'
/>
</div>
<div className='mt-8'>
<ReactQuill modules={{
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ align: [] }],
['clean']
]
}} theme="snow" value={value} onChange={setValue} />
</div>
<div className='mt-8 flex justify-end gap-4'>
<div className='flex flex-col sm:flex-row justify-end gap-3 md:gap-4 pt-4'>
<Button
className='!w-fit px-10 bg-white text-black border border-primary'
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
label={t('new_message.draft')}
/>
<Button
className='w-fit px-10'
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
label={t('new_message.send')}
/>
</div>
</div>
</div>
</>
+11
View File
@@ -1,4 +1,5 @@
import { ReactNode } from "react";
import { RowActionItem } from "../RowActionsDropdown";
export type RowDataType = Record<string, unknown> & { id: string | number };
@@ -12,6 +13,12 @@ export interface ColumnType<T extends RowDataType = RowDataType> {
className?: string;
}
export interface PaginationConfig {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
export interface TableProps<T extends RowDataType = RowDataType> {
columns: ColumnType<T>[];
data: T[];
@@ -27,4 +34,8 @@ export interface TableProps<T extends RowDataType = RowDataType> {
actionsClassName?: string;
actionsPosition?: "top" | "header-replace" | "above-header";
selectable?: boolean;
selectedActions?: ReactNode;
onSelectionChange?: (selectedRows: T[]) => void;
rowActions?: (item: T) => RowActionItem[];
pagination?: PaginationConfig;
}
+11 -13
View File
@@ -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;
+28 -3
View File
@@ -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": "پیش نویس ها"
}
}
+78 -29
View File
@@ -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<string, unknown> {
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<Message[]>([]);
const messageData: Message[] = [
{ id: '1', title: <div className='text-blue-500'>پیش نویس</div>, sender: 'علی محمدی', date: '1402/05/12', read: true },
{ id: '2', title: <div className='text-blue-500'>ارسالی</div>, sender: 'زهرا احمدی', date: '1402/05/13', read: true },
{ id: '3', title: <div className='text-blue-500'>دریافت شده</div>, 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<Message>[] = [
@@ -29,49 +31,94 @@ const TrashList: FC = () => {
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{!item.read && <div className="w-2 h-2 rounded-full bg-blue-500 mr-2" />}
<span>{item.title}</span>
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
<span className="truncate">{item.title}</span>
</div>
)
},
{ key: 'status', title: 'وضعیت' },
{ key: 'sender', title: 'فرستنده' },
{ key: 'date', title: 'تاریخ', align: 'center' },
{
key: 'sender',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-4'>
<Refresh2 size={20} color='black' />
<More size={20} color='black' className='rotate-90' />
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2 size={18} color='black' className="cursor-pointer" />
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<RecoveryConvert size={18} color='black' onClick={() => handleRestoreSelected()} className="cursor-pointer" />
<Trash size={18} color='red' onClick={() => 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: <RecoveryConvert size={16} color="black" />,
onClick: () => console.log('بازگردانی پیام', message.id),
},
{
label: 'حذف دائم',
icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف دائم پیام', message.id),
},
];
return (
<div className='mt-4'>
<h1>{t('draft.title')}</h1>
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg md:text-xl xl:text-2xl font-bold mb-4 md:mb-0">{t('trash.title')}</h1>
<Filters
fields={[
{ type: 'date', name: 'from_date', placeholder: t('received.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('received.to_date') },
{ type: 'date', name: 'from_date', placeholder: t('trash.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('trash.to_date') },
{
type: 'select',
name: 'status',
placeholder: t('received.all'),
placeholder: t('trash.all'),
options: [
{ value: 'all', label: t('received.all') },
{ value: 'read', label: t('received.read') },
{ value: 'unread', label: t('received.unread') }
{ value: 'all', label: t('trash.all') },
{ value: 'read', label: t('trash.read') },
{ value: 'unread', label: t('trash.unread') }
]
},
{ type: 'input', name: 'search', placeholder: t('received.search') }
{ type: 'input', name: 'search', placeholder: t('trash.search') }
]}
onChange={handleFilterChange}
className="mt-8 flex justify-between items-center"
searchField="search"
/>
@@ -81,18 +128,20 @@ const TrashList: FC = () => {
isLoading={isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)}
noDataMessage={
<div className="flex flex-col items-center py-6">
<InfoCircle size={40} className="text-gray-300 mb-2" />
<div>هیچ پیامی یافت نشد</div>
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">سطل آشغال خالی است</div>
</div>
}
selectable={true}
rowActions={getRowActions}
/>
</div>
)
}
export default TrashList
export default List
+78 -30
View File
@@ -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<string, unknown> {
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<Message[]>([]);
const messageData: Message[] = [
{ id: '1', title: <div className='text-blue-500'>پیش نویس</div>, status: <div className='text-yellow-500'>آرشیو شده</div>, sender: 'علی محمدی', date: '1402/05/12', read: true },
{ id: '2', title: <div className='text-blue-500'>ارسالی</div>, status: <div className='text-yellow-500'>آرشیو شده</div>, sender: 'زهرا احمدی', date: '1402/05/13', read: true },
{ id: '3', title: <div className='text-blue-500'>دریافت شده</div>, status: <div className='text-yellow-500'>آرشیو شده</div>, 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<Message>[] = [
@@ -30,49 +31,94 @@ const ArchiveList: FC = () => {
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{!item.read && <div className="w-2 h-2 rounded-full bg-blue-500 mr-2" />}
<span>{item.title}</span>
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
<span className="truncate">{item.title}</span>
</div>
)
},
{ key: 'status', title: 'وضعیت' },
{ key: 'sender', title: 'فرستنده' },
{ key: 'date', title: 'تاریخ', align: 'center' },
{
key: 'sender',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-4'>
<Refresh2 size={20} color='black' />
<More size={20} color='black' className='rotate-90' />
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2 size={18} color='black' className="cursor-pointer" />
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<ArchiveSlash size={18} color='black' onClick={() => handleUnarchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => 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: <ArchiveSlash size={16} color="black" />,
onClick: () => console.log('خروج از آرشیو', message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف پیام', message.id),
},
];
return (
<div className='mt-4'>
<h1>{t('draft.title')}</h1>
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg md:text-xl xl:text-2xl font-bold mb-4 md:mb-0">{t('archive.title')}</h1>
<Filters
fields={[
{ type: 'date', name: 'from_date', placeholder: t('received.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('received.to_date') },
{ type: 'date', name: 'from_date', placeholder: t('archive.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('archive.to_date') },
{
type: 'select',
name: 'status',
placeholder: t('received.all'),
placeholder: t('archive.all'),
options: [
{ value: 'all', label: t('received.all') },
{ value: 'read', label: t('received.read') },
{ value: 'unread', label: t('received.unread') }
{ value: 'all', label: t('archive.all') },
{ value: 'read', label: t('archive.read') },
{ value: 'unread', label: t('archive.unread') }
]
},
{ type: 'input', name: 'search', placeholder: t('received.search') }
{ type: 'input', name: 'search', placeholder: t('archive.search') }
]}
onChange={handleFilterChange}
className="mt-8 flex justify-between items-center"
searchField="search"
/>
@@ -82,18 +128,20 @@ const ArchiveList: FC = () => {
isLoading={isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)}
noDataMessage={
<div className="flex flex-col items-center py-6">
<InfoCircle size={40} className="text-gray-300 mb-2" />
<div>هیچ پیامی یافت نشد</div>
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">هیچ پیام آرشیو شدهای یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
/>
</div>
)
}
export default ArchiveList
export default List
+72 -32
View File
@@ -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<string, unknown> {
id: string;
title: string | ReactNode;
title: string;
sender: string;
date: string;
read: boolean;
@@ -16,11 +17,12 @@ interface Message extends Record<string, unknown> {
const List: FC = () => {
const { t } = useTranslation();
const [isLoading] = useState(false);
const [selectedMessages, setSelectedMessages] = useState<Message[]>([]);
const messageData: Message[] = [
{ id: '1', title: <div className='text-blue-500'>پیش نویس</div>, sender: 'علی محمدی', date: '1402/05/12', read: true },
{ id: '2', title: <div className='text-blue-500'>پیش نویس</div>, sender: 'زهرا احمدی', date: '1402/05/13', read: true },
{ id: '3', title: <div className='text-blue-500'>پیش نویس</div>, 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<Message>[] = [
@@ -29,48 +31,84 @@ const List: FC = () => {
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{!item.read && <div className="w-2 h-2 rounded-full bg-blue-500 mr-2" />}
<span>{item.title}</span>
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
<span className="truncate">{item.title}</span>
</div>
)
},
{ key: 'sender', title: 'فرستنده' },
{ key: 'date', title: 'تاریخ', align: 'center' },
{
key: 'sender',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-4'>
<Refresh2 size={20} color='black' />
<More size={20} color='black' className='rotate-90' />
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2 size={18} color='black' className="cursor-pointer" />
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<Edit size={18} color='black' onClick={() => 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: <Edit size={16} color="black" />,
onClick: () => console.log('ویرایش پیام', message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف پیام', message.id),
},
];
return (
<div className='mt-4'>
<h1>{t('draft.title')}</h1>
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg md:text-xl xl:text-2xl font-bold mb-4 md:mb-0">{t('draft.title')}</h1>
<Filters
fields={[
{ type: 'date', name: 'from_date', placeholder: t('received.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('received.to_date') },
{
type: 'select',
name: 'status',
placeholder: t('received.all'),
options: [
{ value: 'all', label: t('received.all') },
{ value: 'read', label: t('received.read') },
{ value: 'unread', label: t('received.unread') }
]
},
{ type: 'input', name: 'search', placeholder: t('received.search') }
{ type: 'date', name: 'from_date', placeholder: t('draft.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('draft.to_date') },
{ type: 'input', name: 'search', placeholder: t('draft.search') }
]}
onChange={handleFilterChange}
className="mt-8 flex justify-between items-center"
searchField="search"
/>
@@ -80,16 +118,18 @@ const List: FC = () => {
isLoading={isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)}
noDataMessage={
<div className="flex flex-col items-center py-6">
<InfoCircle size={40} className="text-gray-300 mb-2" />
<div>هیچ پیامی یافت نشد</div>
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">هیچ پیشنویسی یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
/>
</div>
)
}
+28 -1
View File
@@ -2,7 +2,34 @@ import { FC } from 'react'
const Home: FC = () => {
return (
<div>home page</div>
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<div className="text-center py-12 md:py-20">
<h1 className="text-2xl md:text-3xl xl:text-4xl font-bold text-gray-800 mb-4">
خوش آمدید به سیستم مدیریت ایمیل
</h1>
<p className="text-sm md:text-base text-gray-600 mb-8 max-w-2xl mx-auto">
از منوی کناری میتوانید به بخشهای مختلف سیستم دسترسی داشته باشید
</p>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 md:gap-6 mt-8 md:mt-12">
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
<h3 className="text-lg md:text-xl font-semibold mb-2">پیامهای دریافتی</h3>
<p className="text-xs md:text-sm text-gray-600">مدیریت ایمیلهای واردشده</p>
</div>
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
<h3 className="text-lg md:text-xl font-semibold mb-2">پیامهای ارسالی</h3>
<p className="text-xs md:text-sm text-gray-600">مشاهده ایمیلهای ارسال شده</p>
</div>
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
<h3 className="text-lg md:text-xl font-semibold mb-2">پیشنویسها</h3>
<p className="text-xs md:text-sm text-gray-600">ادامه نوشتن ایمیلهای ناتمام</p>
</div>
<div className="bg-white p-4 md:p-6 rounded-xl md:rounded-2xl shadow-sm border">
<h3 className="text-lg md:text-xl font-semibold mb-2">تنظیمات</h3>
<p className="text-xs md:text-sm text-gray-600">پیکربندی سیستم و حساب کاربری</p>
</div>
</div>
</div>
</div>
)
}
+80 -13
View File
@@ -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<string, unknown> {
id: string;
@@ -16,6 +17,7 @@ interface Message extends Record<string, unknown> {
const List: FC = () => {
const { t } = useTranslation();
const [isLoading] = useState(false);
const [selectedMessages, setSelectedMessages] = useState<Message[]>([]);
const messageData: Message[] = [
{ id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true },
@@ -29,29 +31,87 @@ const List: FC = () => {
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{!item.read && <div className="w-2 h-2 rounded-full bg-blue-500 mr-2" />}
<span>{item.title}</span>
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
<span className="truncate">{item.title}</span>
</div>
)
},
{ key: 'sender', title: 'فرستنده' },
{ key: 'date', title: 'تاریخ', align: 'center' },
{
key: 'sender',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-4'>
<Refresh2 size={20} color='black' />
<More size={20} color='black' className='rotate-90' />
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2 size={18} color='black' className="cursor-pointer" />
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Edit size={18} color='black' onClick={() => 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: <ArchiveSlash size={16} color="black" />,
onClick: () => console.log('خروج از آرشیو', message.id),
},
{
label: 'خوانده شده',
icon: <Edit size={16} color="black" />,
onClick: () => console.log('علامت‌گذاری به عنوان خوانده شده', message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف پیام', message.id),
},
];
return (
<div className='mt-4'>
<h1>{t('received.title')}</h1>
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg md:text-xl xl:text-2xl font-bold mb-4 md:mb-0">{t('received.title')}</h1>
<Filters
fields={[
@@ -70,7 +130,6 @@ const List: FC = () => {
{ 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={
<div className="flex flex-col items-center py-6">
<InfoCircle size={40} className="text-gray-300 mb-2" />
<div>هیچ پیامی یافت نشد</div>
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
pagination={{
totalPages: 10,
currentPage: 1,
onPageChange: () => { }
}}
/>
</div>
+24 -12
View File
@@ -29,19 +29,32 @@ const List: FC = () => {
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{!item.read && <div className="w-2 h-2 rounded-full bg-blue-500 mr-2" />}
<span>{item.title}</span>
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />}
<span className="truncate">{item.title}</span>
</div>
)
},
{ key: 'sender', title: 'فرستنده' },
{ key: 'date', title: 'تاریخ', align: 'center' },
{
key: 'sender',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-4'>
<Refresh2 size={20} color='black' />
<More size={20} color='black' className='rotate-90' />
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2 size={18} color='black' className="cursor-pointer" />
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
@@ -50,8 +63,8 @@ const List: FC = () => {
};
return (
<div className='mt-4'>
<h1>{t('sent.title')}</h1>
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg md:text-xl xl:text-2xl font-bold mb-4 md:mb-0">{t('sent.title')}</h1>
<Filters
fields={[
@@ -70,7 +83,6 @@ const List: FC = () => {
{ 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={
<div className="flex flex-col items-center py-6">
<InfoCircle size={40} className="text-gray-300 mb-2" />
<div>هیچ پیامی یافت نشد</div>
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
}
selectable={true}
+14 -10
View File
@@ -23,9 +23,13 @@ const Setting: FC = () => {
return <Address />;
case SettingTabEnum.SETTING_PERSONALITY:
return (
<div className='flex flex-row-reverse gap-6 mt-8'>
<PersonalitySidebar />
<Personality />
<div className='flex flex-col xl:flex-row-reverse gap-4 md:gap-6 mt-6 md:mt-8'>
<div className="xl:w-auto w-full">
<PersonalitySidebar />
</div>
<div className="flex-1">
<Personality />
</div>
</div>
);
default:
@@ -34,29 +38,29 @@ const Setting: FC = () => {
};
return (
<div className='mt-4'>
<h1 className='text-2xl font-bold text-right'>{t('setting.title')}</h1>
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className='text-lg md:text-xl xl:text-2xl font-bold text-right mb-4 md:mb-0'>{t('setting.title')}</h1>
<div className='mt-7'>
<div className='mt-4 md:mt-7'>
<Tabs
items={[
{
icon: <Setting3 size={22} color={activeTab === SettingTabEnum.SETTING_MAIL_SERVER ? 'black' : '#8C90A3'} />,
icon: <Setting3 size={20} color={activeTab === SettingTabEnum.SETTING_MAIL_SERVER ? 'black' : '#8C90A3'} />,
label: t('setting.mail_server'),
value: SettingTabEnum.SETTING_MAIL_SERVER
},
{
icon: <Global size={22} color={activeTab === SettingTabEnum.SETTING_DOMAIN ? 'black' : '#8C90A3'} />,
icon: <Global size={20} color={activeTab === SettingTabEnum.SETTING_DOMAIN ? 'black' : '#8C90A3'} />,
label: t('setting.domain'),
value: SettingTabEnum.SETTING_DOMAIN
},
{
icon: <People size={22} color={activeTab === SettingTabEnum.SETTING_ADDRESS ? 'black' : '#8C90A3'} />,
icon: <People size={20} color={activeTab === SettingTabEnum.SETTING_ADDRESS ? 'black' : '#8C90A3'} />,
label: t('setting.address'),
value: SettingTabEnum.SETTING_ADDRESS
},
{
icon: <Brush2 size={22} color={activeTab === SettingTabEnum.SETTING_PERSONALITY ? 'black' : '#8C90A3'} />,
icon: <Brush2 size={20} color={activeTab === SettingTabEnum.SETTING_PERSONALITY ? 'black' : '#8C90A3'} />,
label: t('setting.personality'),
value: SettingTabEnum.SETTING_PERSONALITY
}
+5 -5
View File
@@ -27,9 +27,9 @@ const Header: FC = () => {
}, [location.pathname]);
return (
<div className='fixed z-10 right-4 left-4 xl:right-[285px] top-4 xl:h-16 h-12 flex items-center px-6 bg-white justify-between rounded-[32px] xl:w-[calc(100%-305px)]'>
<div className='fixed z-10 right-2 left-2 md:right-4 md:left-4 xl:right-[285px] top-2 md:top-4 xl:h-16 h-12 flex items-center px-3 md:px-6 bg-white justify-between rounded-[20px] md:rounded-[32px] xl:w-[calc(100%-305px)]'>
<div className='min-w-[270px] hidden xl:block'>
<div className='min-w-[200px] md:min-w-[270px] hidden xl:block'>
<Input
variant='search'
placeholder={t('header.search')}
@@ -40,12 +40,12 @@ const Header: FC = () => {
<HambergerMenu size={24} color='black' />
</div>
{/* <img src={LogoImage} className='h-6 xl:hidden block absolute right-0 left-0 mx-auto' /> */}
<div className='flex xl:gap-6 gap-4 items-center'>
<div className='flex xl:gap-6 gap-3 md:gap-4 items-center'>
<Link to={Paths.home}>
<Element3 color='black' className='xl:size-[18px] size-[17px]' />
<Element3 color='black' className='size-4 md:size-[17px] xl:size-[18px]' />
</Link>
<Link className='xl:hidden' to={Paths.home}>
<Wallet className='xl:size-[18px] size-[17px]' color='black' />
<Wallet className='size-4 md:size-[17px] xl:size-[18px]' color='black' />
</Link>
<Link className='hidden xl:block' to={Paths.home}>
<div className='flex items-center h-8 pl-2 rounded-full bg-[#EEF0F7]'>
+4 -4
View File
@@ -5,12 +5,12 @@ import Header from './Header'
const Main: FC = () => {
return (
<div className='p-4 overflow-hidden'>
<div className='p-2 md:p-4 overflow-hidden'>
<SideBar />
<Header />
<div className='flex-1 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
<div className={`overflow-auto w-[${window.innerWidth}] max-h-[calc(100vh-113px)]`}>
<div className='xl:pb-20 pb-32'>
<div className='flex-1 ms-0 xl:ms-[269px] mt-[68px] xl:mt-[81px]'>
<div className='overflow-auto w-full max-h-[calc(100vh-113px)] md:max-h-[calc(100vh-113px)]'>
<div className='pb-20 md:pb-24 xl:pb-20'>
<AppRouter />
</div>
</div>
+15 -12
View File
@@ -23,32 +23,35 @@ const SideBar: FC = () => {
return (
<>
{
openSidebar && <div className='fixed top-0 left-0 right-0 bottom-0 bg-black bg-opacity-50 z-10' onClick={() => setOpenSidebar(false)} />
openSidebar && <div className='fixed top-0 left-0 right-0 bottom-0 bg-black/50 z-20' onClick={() => setOpenSidebar(false)} />
}
<div
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-4 right-0 xl:top-4 top-0 xl:bottom-4 bottom-0 xl:rounded-[32px] w-[250px] bg-white flex-col py-12',
'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',
openSidebar && 'opacity-100 visible -translate-x-[0px] block z-40'
)}
>
<div className='flex justify-center'>
<img src={LogoImage} className='w-[140px]' />
<div className='flex justify-center px-4'>
<img src={LogoImage} className='w-[120px] md:w-[140px]' />
</div>
<div className='flex justify-center mt-14'>
<div className='flex justify-center mt-10 md:mt-14 px-4'>
<Button
className='bg-secondary font-normal text-primary w-fit px-6'
onClick={() => setOpenNewMessage(true)}
className='bg-secondary font-normal text-primary w-fit px-4 md:px-6 text-sm'
onClick={() => {
setOpenSidebar(false)
setOpenNewMessage(true)
}}
>
<div className='flex gap-2'>
<Add size={20} color='black' />
<div className='flex gap-2 items-center'>
<Add size={18} color='black' />
<div>{t('sidebar.new_message')}</div>
</div>
</Button>
</div>
<div className='flex-1 flex flex-col h-full overflow-y-auto no-scrollbar'>
<div className='mt-10 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')}
</div>
@@ -89,7 +92,7 @@ const SideBar: FC = () => {
/>
</div>
<div className='mt-10 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.other')}
</div>
@@ -109,7 +112,7 @@ const SideBar: FC = () => {
/>
</div>
<div className='flex-1 flex flex-col justify-end mt-14 pb-8'>
<div className='flex-1 flex flex-col justify-end mt-10 md:mt-14 pb-6 md:pb-8'>
<div className='text-xs text-[#8C90A3]'>
<SideBarItem
icon={<Setting2 variant={isActive(Paths.setting) ? 'Bold' : 'Outline'} color={isActive(Paths.setting) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
+7 -4
View File
@@ -21,17 +21,20 @@ const SideBarItem: FC<Props> = (props: Props) => {
}
return (
<Link onClick={props.isLogout ? handleLogout : undefined} to={props.link} className='flex text-xs gap-9 mt-4'>
<Link onClick={props.isLogout ? handleLogout : undefined} to={props.link} className='flex text-xs gap-6 md:gap-8 xl:gap-9 mt-3 md:mt-4 px-2 md:px-0'>
{
!props.isWithoutLine &&
<div className={clx(
'w-1 bg-black h-6',
'w-1 bg-black h-5 md:h-6',
!props.isActive && 'invisible'
)}></div>
}
<div className='flex gap-2.5 items-center'>
<div className='flex gap-2 md:gap-2.5 items-center'>
{props.icon}
<div className={props.isActive ? 'text-black' : ''}>
<div className={clx(
'text-xs md:text-sm',
props.isActive ? 'text-black font-medium' : ''
)}>
{props.title}
</div>
</div>