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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user