From 3b382a973b0102936b84b1cb1bb3da373ccad46c Mon Sep 17 00:00:00 2001 From: hamid zarghami Date: Sat, 18 Oct 2025 10:38:12 +0330 Subject: [PATCH] component table + orders in home --- @/components/ui/checkbox.tsx | 28 ++ components.json | 20 + package-lock.json | 229 ++++++++++- package.json | 2 + src/App.tsx | 1 + src/components/DefaultTableSkeleton.tsx | 30 ++ src/components/Pagination.tsx | 109 ++++++ src/components/RowActionsDropdown.tsx | 145 +++++++ src/components/Table.tsx | 363 ++++++++++++++++++ src/components/Td.tsx | 23 ++ src/components/types/TableTypes.ts | 47 +++ src/components/ui/checkbox.tsx | 37 ++ src/index.css | 4 + src/pages/home/Home.tsx | 2 + .../home/components/OrderTableComponents.tsx | 106 +++++ src/pages/home/components/Orders.tsx | 64 +++ src/pages/home/components/data/ordersData.ts | 37 ++ src/pages/home/components/types/OrderTypes.ts | 22 ++ 18 files changed, 1268 insertions(+), 1 deletion(-) create mode 100644 @/components/ui/checkbox.tsx create mode 100644 components.json create mode 100644 src/components/DefaultTableSkeleton.tsx create mode 100644 src/components/Pagination.tsx create mode 100644 src/components/RowActionsDropdown.tsx create mode 100644 src/components/Table.tsx create mode 100644 src/components/Td.tsx create mode 100644 src/components/types/TableTypes.ts create mode 100644 src/components/ui/checkbox.tsx create mode 100644 src/pages/home/components/OrderTableComponents.tsx create mode 100644 src/pages/home/components/Orders.tsx create mode 100644 src/pages/home/components/data/ordersData.ts create mode 100644 src/pages/home/components/types/OrderTypes.ts diff --git a/@/components/ui/checkbox.tsx b/@/components/ui/checkbox.tsx new file mode 100644 index 0000000..f9d8860 --- /dev/null +++ b/@/components/ui/checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/helpers/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/components.json b/components.json new file mode 100644 index 0000000..65b6067 --- /dev/null +++ b/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "slate", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/helpers/utils", + "ui": "@/components/ui", + "lib": "@/helpers", + "hooks": "@/hooks" + } +} diff --git a/package-lock.json b/package-lock.json index 9c1ad4c..d8f445f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@headlessui/react": "^2.2.9", + "@radix-ui/react-checkbox": "^1.3.3", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.90.3", "axios": "^1.12.2", @@ -16,6 +17,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-infinite-scroll-component": "^6.1.0", + "react-loading-skeleton": "^3.5.0", "react-router-dom": "^7.9.4", "react-spinners": "^0.17.0", "swiper": "^12.0.2", @@ -1115,6 +1117,222 @@ "node": ">= 8" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@react-aria/focus": { "version": "3.21.2", "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz", @@ -1911,7 +2129,7 @@ "version": "19.2.2", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", - "dev": true, + "devOptional": true, "license": "MIT", "peerDependencies": { "@types/react": "^19.2.0" @@ -4062,6 +4280,15 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-loading-skeleton": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/react-loading-skeleton/-/react-loading-skeleton-3.5.0.tgz", + "integrity": "sha512-gxxSyLbrEAdXTKgfbpBEFZCO/P153DnqSCQau2+o6lNy1jgMRr2MmRmOzMmyrwSaSYLRB8g7b0waYPmUjz7IhQ==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", diff --git a/package.json b/package.json index f833050..3dbd250 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@headlessui/react": "^2.2.9", + "@radix-ui/react-checkbox": "^1.3.3", "@tailwindcss/vite": "^4.1.14", "@tanstack/react-query": "^5.90.3", "axios": "^1.12.2", @@ -18,6 +19,7 @@ "react": "^19.2.0", "react-dom": "^19.2.0", "react-infinite-scroll-component": "^6.1.0", + "react-loading-skeleton": "^3.5.0", "react-router-dom": "^7.9.4", "react-spinners": "^0.17.0", "swiper": "^12.0.2", diff --git a/src/App.tsx b/src/App.tsx index ae619e2..f6c0015 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,7 @@ import { type FC } from 'react' import '@/assets/fonts/irancell/style.css' import 'swiper/swiper-bundle.css'; +import 'react-loading-skeleton/dist/skeleton.css' import { BrowserRouter } from 'react-router-dom' // import { getToken } from './config/func' import MainRouter from './router/MainRouter' diff --git a/src/components/DefaultTableSkeleton.tsx b/src/components/DefaultTableSkeleton.tsx new file mode 100644 index 0000000..867f975 --- /dev/null +++ b/src/components/DefaultTableSkeleton.tsx @@ -0,0 +1,30 @@ +import { type FC, Fragment } from 'react' +import Td from './Td' +import Skeleton from 'react-loading-skeleton' + +type Props = { + tdCount: number, + trCount?: number, +} + +const DefaultTableSkeleton: FC = ({ tdCount, trCount = 5 }) => { + return ( + + { + Array.from({ length: trCount }).map((_, rowIndex) => ( + + { + Array.from({ length: tdCount }).map((_, colIndex) => ( + + + + )) + } + + )) + } + + ) +} + +export default DefaultTableSkeleton diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx new file mode 100644 index 0000000..d381ceb --- /dev/null +++ b/src/components/Pagination.tsx @@ -0,0 +1,109 @@ +import React from "react"; + +interface PaginationProps { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +const Pagination: React.FC = ({ + currentPage, + totalPages, + onPageChange, +}) => { + // اگر فقط یک صفحه داریم، pagination نمایش داده نمی‌شود + if (totalPages <= 1) return null; + + const getPageNumbers = () => { + const pageNumbers: (number | string)[] = []; + const maxVisiblePages = window.innerWidth < 768 ? 3 : 5; // تعداد کمتر برای موبایل + + if (totalPages <= maxVisiblePages) { + for (let i = 1; i <= totalPages; i++) { + pageNumbers.push(i); + } + } else { + // نمایش صفحه اول + pageNumbers.push(1); + + if (currentPage > 3) { + pageNumbers.push("..."); + } + + // صفحات میانی + const start = Math.max(2, Math.min(currentPage - 1, totalPages - 3)); + const end = Math.min(totalPages - 1, Math.max(currentPage + 1, 4)); + + for (let i = start; i <= end; i++) { + if (!pageNumbers.includes(i)) { + pageNumbers.push(i); + } + } + + if (currentPage < totalPages - 2) { + pageNumbers.push("..."); + } + + // نمایش صفحه آخر + if (!pageNumbers.includes(totalPages)) { + pageNumbers.push(totalPages); + } + } + + return pageNumbers; + }; + + const pageNumbers = getPageNumbers(); + + return ( +
+ {/* دکمه صفحه قبل */} + + + {/* شماره صفحات */} +
+ {pageNumbers.map((page, index) => + typeof page === "number" ? ( + + ) : ( + + {page} + + ) + )} +
+ + {/* دکمه صفحه بعد */} + +
+ ); +}; + +export default Pagination; \ No newline at end of file diff --git a/src/components/RowActionsDropdown.tsx b/src/components/RowActionsDropdown.tsx new file mode 100644 index 0000000..06aa578 --- /dev/null +++ b/src/components/RowActionsDropdown.tsx @@ -0,0 +1,145 @@ +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; + trigger?: React.ReactNode; + topOffset?: number; + leftOffset?: number; + topOffsetMobile?: number; + leftOffsetMobile?: number; +} + +const RowActionsDropdown: React.FC = ({ + actions, + className = '', + trigger, + topOffset = 0, + leftOffset = 0, + topOffsetMobile = 0, + leftOffsetMobile = 0 +}) => { + const [isOpen, setIsOpen] = useState(false) + const [dropdownPosition, setDropdownPosition] = useState({ top: 0, left: 0 }) + const dropdownRef = useRef(null) + const buttonRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setIsOpen(false) + } + } + + document.addEventListener('mousedown', handleClickOutside) + return () => { + document.removeEventListener('mousedown', handleClickOutside) + } + }, []) + + const calculatePosition = (rect: DOMRect) => { + const isMobile = window.innerWidth < 768 + const dropdownWidth = isMobile ? 140 : 150 + const dropdownHeight = actions.length * (isMobile ? 36 : 40) + + // انتخاب offset بر اساس سایز صفحه + const currentTopOffset = isMobile ? topOffsetMobile : topOffset + const currentLeftOffset = isMobile ? leftOffsetMobile : leftOffset + + let left = rect.left + window.scrollX - dropdownWidth + rect.width + currentLeftOffset + let top = rect.bottom + window.scrollY + currentTopOffset + + if (left + dropdownWidth > window.innerWidth) { + left = rect.left + window.scrollX - dropdownWidth + currentLeftOffset + } + + if (left < 0) { + left = isMobile ? 20 : 40 + } + + if (top + dropdownHeight > window.innerHeight + window.scrollY) { + top = rect.top + window.scrollY - dropdownHeight + currentTopOffset + } + + // اطمینان از اینکه dropdown از صفحه خارج نشود + if (left < 0) left = 20 + if (left + dropdownWidth > window.innerWidth) left = window.innerWidth - dropdownWidth - 20 + if (top < 0) top = 20 + if (top + dropdownHeight > window.innerHeight) top = window.innerHeight - dropdownHeight - 20 + + return { top, left } + } + + const handleToggle = (e: React.MouseEvent) => { + e.stopPropagation() + + if (!isOpen && buttonRef.current) { + const rect = buttonRef.current.getBoundingClientRect() + const calculatedPosition = calculatePosition(rect) + setDropdownPosition(calculatedPosition) + } + + setIsOpen(!isOpen) + } + + const handleActionClick = (action: RowActionItem) => { + action.onClick() + setIsOpen(false) + } + + const renderDropdown = () => { + if (!isOpen) return null + + return createPortal( +
+ {actions.map((action, index) => ( + + ))} +
, + document.body + ) + } + + return ( +
+ + + {renderDropdown()} +
+ ) +} + +export default RowActionsDropdown \ No newline at end of file diff --git a/src/components/Table.tsx b/src/components/Table.tsx new file mode 100644 index 0000000..56d691a --- /dev/null +++ b/src/components/Table.tsx @@ -0,0 +1,363 @@ +import React, { Fragment, useState } from 'react'; +import DefaultTableSkeleton from '@/components/DefaultTableSkeleton'; +import Td from '@/components/Td'; +import type { TableProps, RowDataType, ColumnType } from '@/components/types/TableTypes'; +import { Checkbox } from '@/components/ui/checkbox'; +import RowActionsDropdown from '@/components/RowActionsDropdown'; +import Pagination from '@/components/Pagination'; + +const Table = ({ + columns, + data, + isLoading = false, + onRowClick, + className = '', + rowClassName = '', + noDataMessage = 'هیچ داده‌ای یافت نشد', + headerClassName = 'bg-gray-50', + emptyRowsCount = 5, + showHeader = true, + actions = null, + actionsPosition = 'top', + selectable = false, + selectedActions = null, + onSelectionChange, + rowActions, + pagination, +}: TableProps): React.ReactElement => { + + const [selectedRows, setSelectedRows] = useState([]); + + const getRowClassName = (item: T, index: number): string => { + if (typeof rowClassName === 'function') { + return rowClassName(item, index); + } + return rowClassName; + }; + + const getCellClassName = (column: ColumnType): string => { + const alignClass = column.align ? `text-${column.align}` : ''; + return `px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${alignClass} ${column.className || ''}`.trim(); + }; + + const handleRowClick = (item: T) => { + if (onRowClick) { + onRowClick(item.id, item); + } + }; + + const handleSelectAll = () => { + const dataArray = data || []; + const newSelectedRows = selectedRows.length === dataArray.length ? [] : [...dataArray]; + 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); + }; + + // Gmail-style layout برای وقتی header نمایش داده نمی‌شود + const renderGmailStyleRows = () => { + const hasActions = (actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions)); + const roundedClass = hasActions ? 'rounded-b-2xl' : 'rounded-2xl'; + + if (isLoading) { + return ( +
+ {Array.from({ length: emptyRowsCount }).map((_, index) => ( +
+ ))} +
+ ); + } + + if (!data || data.length === 0) { + return ( +
+ {noDataMessage} +
+ ); + } + + return ( +
+ {data.map((item, rowIndex) => ( +
handleRowClick(item)} + > + {selectable && ( +
e.stopPropagation()} + > + handleSingleRowSelect(item, !!checked)} + /> +
+ )} + +
+ {/* محتوای اصلی - ستون اول */} +
+ {columns[0]?.render ? columns[0].render(item, rowIndex) : (item[columns[0]?.key] as React.ReactNode)} +
+ + {/* اطلاعات اضافی - سه ستون آخر در یک خط */} + {columns.length > 1 && ( +
+ {columns.slice(1).map((col) => ( +
+ {col.render ? col.render(item, rowIndex) : (item[col.key] as React.ReactNode)} +
+ ))} +
+ )} +
+ + {rowActions && ( +
e.stopPropagation()} + > + +
+ )} +
+ ))} +
+ ); + }; + + const renderTableBody = () => { + if (isLoading) { + return ( + + + + ); + } + + if (!data || data.length === 0) { + return ( + + + {noDataMessage} + + + ); + } + return (data || []).map((item, rowIndex) => ( + handleRowClick(item)} + > + {selectable && ( + e.stopPropagation()} + > + handleSingleRowSelect(item, !!checked)} + /> + + )} + {columns.map((col) => ( + + {col.render ? col.render(item, rowIndex) : item[col.key] as React.ReactNode} + + ))} + {rowActions && ( + e.stopPropagation()} + > + + + )} + + )); + }; + + const renderHeader = () => { + if (!showHeader || actionsPosition === 'header-replace') return null; + + return ( + + + {selectable && ( + e.stopPropagation()} + > + 0 && selectedRows.length === (data?.length || 0)} + onCheckedChange={handleSelectAll} + /> + + )} + {columns.map((col) => ( + + ))} + {rowActions && ( + + )} + + + ); + }; + + const renderActions = () => { + if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null; + + return ( +
+
+ {actions} + {selectable && selectedRows.length > 0 && selectedActions && selectedActions} +
+
+ ); + }; + + // اگر header نشان داده نمی‌شود، از Gmail-style layout فقط در موبایل استفاده کن + if (!showHeader) { + return ( +
+ {/* نسخه موبایل - Gmail style */} +
+ {(actionsPosition === 'top' || actionsPosition === 'above-header') && ( +
+
+ {actions} + {selectable && selectedRows.length > 0 && selectedActions && selectedActions} +
+
+ )} + {renderGmailStyleRows()} +
+ + {/* نسخه دسکتاپ - Table معمولی */} +
+ {(actionsPosition === 'top' || actionsPosition === 'above-header') && ( +
+
+ {actions} + {selectable && selectedRows.length > 0 && selectedActions && selectedActions} +
+
+ )} +
0 && selectedActions)) ? 'rounded-b-3xl' : 'rounded-3xl'} bg-white`}> + + + {renderTableBody()} + +
+
+
+ + {pagination && ( + + )} +
+ ); + } + + return ( +
+ {(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()} + +
+ + {renderHeader()} + {actionsPosition === 'header-replace' && ( + + + + + + )} + + {renderTableBody()} + +
+ {renderActions()} +
+
+ + {pagination && ( + + )} +
+ ); +}; + +export default Table; + +/* +مثال استفاده با pagination: + +import Table from '@/components/Table'; +import { useState } from 'react'; + +const MyComponent = () => { + const [currentPage, setCurrentPage] = useState(1); + const totalPages = 10; + + const columns = [ + { title: 'نام', key: 'name' }, + { title: 'ایمیل', key: 'email' }, + ]; + + const data = [ + { id: 1, name: 'علی', email: 'ali@example.com' }, + { id: 2, name: 'فاطمه', email: 'fateme@example.com' }, + ]; + + const handlePageChange = (page: number) => { + setCurrentPage(page); + // اینجا می‌توانید API call جدید برای دریافت داده‌های صفحه جدید بزنید + }; + + return ( + + ); +}; +*/ \ No newline at end of file diff --git a/src/components/Td.tsx b/src/components/Td.tsx new file mode 100644 index 0000000..b29b84a --- /dev/null +++ b/src/components/Td.tsx @@ -0,0 +1,23 @@ +import type { FC, ReactNode } from 'react' + +interface Props { + text: string | number | ReactNode, + children?: ReactNode, + dir?: string, + className?: string, +} + +const Td: FC = (props: Props) => { + return ( + + ) +} + +export default Td \ No newline at end of file diff --git a/src/components/types/TableTypes.ts b/src/components/types/TableTypes.ts new file mode 100644 index 0000000..5d87e0c --- /dev/null +++ b/src/components/types/TableTypes.ts @@ -0,0 +1,47 @@ +export interface RowDataType { + id: string | number; + [key: string]: unknown; +} + +export interface ColumnType { + title: string; + key: string; + align?: "left" | "center" | "right"; + className?: string; + width?: string | number; + render?: (item: T, index?: number) => React.ReactNode; +} + +export interface PaginationType { + currentPage: number; + totalPages: number; + onPageChange: (page: number) => void; +} + +export interface ActionType { + label: string; + onClick: () => void; + icon?: React.ReactNode; + className?: string; + disabled?: boolean; +} + +export interface TableProps { + columns: ColumnType[]; + data?: T[]; + isLoading?: boolean; + onRowClick?: (id: string | number, item: T) => void; + className?: string; + rowClassName?: string | ((item: T, index: number) => string); + noDataMessage?: string; + headerClassName?: string; + emptyRowsCount?: number; + showHeader?: boolean; + actions?: React.ReactNode; + actionsPosition?: "top" | "above-header" | "bottom" | "header-replace"; + selectable?: boolean; + selectedActions?: React.ReactNode; + onSelectionChange?: (selectedRows: T[]) => void; + rowActions?: (item: T) => ActionType[]; + pagination?: PaginationType; +} diff --git a/src/components/ui/checkbox.tsx b/src/components/ui/checkbox.tsx new file mode 100644 index 0000000..ae941bf --- /dev/null +++ b/src/components/ui/checkbox.tsx @@ -0,0 +1,37 @@ +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" + +import { clx } from "@/helpers/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/src/index.css b/src/index.css index e2eec48..b7c74c0 100644 --- a/src/index.css +++ b/src/index.css @@ -45,3 +45,7 @@ textarea::placeholder { --color-border: #f5f7fc; --color-description: #c3c7dd; } + +tbody tr:nth-child(odd) { + background-color: #f5f7fc; +} diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index 7bcfc48..193d64b 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -1,6 +1,7 @@ import { type FC } from 'react' import Stats from './components/Stats' import Services from './components/Services' +import Orders from './components/Orders' const Home: FC = () => { return ( @@ -8,6 +9,7 @@ const Home: FC = () => {
+
) diff --git a/src/pages/home/components/OrderTableComponents.tsx b/src/pages/home/components/OrderTableComponents.tsx new file mode 100644 index 0000000..513e07b --- /dev/null +++ b/src/pages/home/components/OrderTableComponents.tsx @@ -0,0 +1,106 @@ +import { type FC } from 'react'; +import { Eye } from 'iconsax-react'; +import StatusCircle from '@/components/StatusCircle'; +import { type Order, type OrderStatus } from './types/OrderTypes'; + +// کامپوننت رندر شماره سفارش +export const OrderNumberRenderer: FC<{ order: Order }> = ({ order }) => { + return ( + + {order.number} + + ); +}; + +// کامپوننت رندر عنوان سفارش +export const OrderTitleRenderer: FC<{ order: Order }> = ({ order }) => { + return ( + + {order.title} + + ); +}; + +// کامپوننت رندر تاریخ +export const OrderDateRenderer: FC<{ order: Order }> = ({ order }) => { + return ( +
+
+ ({order.creationDate.gregorian}) +
+
+ {order.creationDate.persian} +
+
+ ); +}; + +// کامپوننت رندر وضعیت سفارش +export const OrderStatusRenderer: FC<{ order: Order }> = ({ order }) => { + const getStatusConfig = (status: OrderStatus) => { + switch (status) { + case 'در حال طراحی': + return { + color: 'text-blue-600', + bgColor: 'bg-blue-50' + }; + case 'تهیه متریال': + return { + color: 'text-gray-600', + bgColor: 'bg-gray-50' + }; + case 'در حال چاپ': + return { + color: 'text-orange-600', + bgColor: 'bg-orange-50' + }; + case 'تکمیل شده': + return { + color: 'text-green-600', + bgColor: 'bg-green-50' + }; + default: + return { + color: 'text-gray-600', + bgColor: 'bg-gray-50' + }; + } + }; + + const config = getStatusConfig(order.status); + + return ( + + {order.status} + + ); +}; + +// کامپوننت رندر پیام‌ها +export const OrderMessagesRenderer: FC<{ order: Order }> = ({ order }) => { + const hasUnreadMessages = order.unreadMessages > 0; + + return ( +
+ + + {order.unreadMessages} پیام خوانده نشده + +
+ ); +}; + +// کامپوننت رندر آیکون مشاهده +export const OrderViewRenderer: FC<{ order: Order }> = ({ order }) => { + return ( + + ); +}; diff --git a/src/pages/home/components/Orders.tsx b/src/pages/home/components/Orders.tsx new file mode 100644 index 0000000..1f01dea --- /dev/null +++ b/src/pages/home/components/Orders.tsx @@ -0,0 +1,64 @@ +import Table from '@/components/Table' +import { type FC } from 'react' +import { type ColumnType } from '@/components/types/TableTypes' +import { type Order } from './types/OrderTypes' +import { ordersData } from './data/ordersData' +import { + OrderNumberRenderer, + OrderTitleRenderer, + OrderDateRenderer, + OrderStatusRenderer, + OrderMessagesRenderer, + OrderViewRenderer +} from './OrderTableComponents' + +const Orders: FC = () => { + const columns: ColumnType[] = [ + { + title: 'شماره', + key: 'number', + render: (order) => + }, + { + title: 'عنوان سفارش', + key: 'title', + render: (order) => + }, + { + title: 'تاریخ ایجاد', + key: 'creationDate', + render: (order) => + }, + { + title: 'وضعیت سفارش', + key: 'status', + render: (order) => + }, + { + title: 'پیام ها', + key: 'unreadMessages', + render: (order) => + }, + { + title: '', + key: 'actions', + render: (order) => , + width: '60px' + } + ] + + return ( +
+

+ سفارش های در حال انجام +

+ + + columns={columns} + data={ordersData} + /> +
+ ) +} + +export default Orders \ No newline at end of file diff --git a/src/pages/home/components/data/ordersData.ts b/src/pages/home/components/data/ordersData.ts new file mode 100644 index 0000000..d605614 --- /dev/null +++ b/src/pages/home/components/data/ordersData.ts @@ -0,0 +1,37 @@ +import { type Order } from '../types/OrderTypes'; + +export const ordersData: Order[] = [ + { + id: 1, + number: '123455', + title: 'کارت ویزیت', + creationDate: { + gregorian: '2024-11-01', + persian: '1403/09/01' + }, + status: 'در حال طراحی', + unreadMessages: 2 + }, + { + id: 2, + number: '123455', + title: 'شاپینگ بگ', + creationDate: { + gregorian: '2024-11-01', + persian: '1403/09/01' + }, + status: 'تهیه متریال', + unreadMessages: 0 + }, + { + id: 3, + number: '123455', + title: 'کارت ویزیت', + creationDate: { + gregorian: '2024-11-01', + persian: '1403/09/01' + }, + status: 'در حال چاپ', + unreadMessages: 2 + } +]; diff --git a/src/pages/home/components/types/OrderTypes.ts b/src/pages/home/components/types/OrderTypes.ts new file mode 100644 index 0000000..7f25d48 --- /dev/null +++ b/src/pages/home/components/types/OrderTypes.ts @@ -0,0 +1,22 @@ +import { type RowDataType } from '@/components/types/TableTypes'; + +export interface Order extends RowDataType { + id: string | number; + number: string; + title: string; + creationDate: { + gregorian: string; + persian: string; + }; + status: OrderStatus; + unreadMessages: number; + [key: string]: unknown; +} + +export type OrderStatus = 'در حال طراحی' | 'تهیه متریال' | 'در حال چاپ' | 'تکمیل شده'; + +export interface OrderStatusConfig { + status: OrderStatus; + color: string; + textColor: string; +}
+ { + props.text ? + props.text + : + props.children + } +