reponsive and selective actions and row actioin + pagination
This commit is contained in:
+27
-27
@@ -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
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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' />
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
@@ -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
@@ -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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
*/
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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} />}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user