component table + orders in home
This commit is contained in:
@@ -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<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid place-content-center peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("grid place-content-center text-current")}
|
||||
>
|
||||
<Check className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Generated
+228
-1
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<Props> = ({ tdCount, trCount = 5 }) => {
|
||||
return (
|
||||
<Fragment>
|
||||
{
|
||||
Array.from({ length: trCount }).map((_, 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}>
|
||||
<Skeleton />
|
||||
</Td>
|
||||
))
|
||||
}
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
export default DefaultTableSkeleton
|
||||
@@ -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,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<RowActionsDropdownProps> = ({
|
||||
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<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 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(
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
className="fixed py-3 md:py-3 bg-white dark:bg-[#252526] rounded-xl md:rounded-2xl shadow-lg z-[5] min-w-[140px] md:min-w-[150px] border border-[#d0d0d0]"
|
||||
style={{
|
||||
top: `${dropdownPosition.top}px`,
|
||||
left: `${dropdownPosition.left}px`,
|
||||
}}
|
||||
>
|
||||
{actions.map((action, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => handleActionClick(action)}
|
||||
className={`w-full mt-1 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 dark:hover:bg-[#2d2d30] ${index === 0 ? 'rounded-t-lg' : ''} ${action.className || ''}`}
|
||||
>
|
||||
{action.icon && <span className="ml-2">{action.icon}</span>}
|
||||
<span className={`${action.label === 'حذف' ? 'text-red-500' : ''} dark:text-gray-200`}>{action.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
onClick={handleToggle}
|
||||
className="mt-2 hover:bg-gray-100 dark:hover:bg-[#2d2d30] rounded-full transition-colors"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
{trigger ? trigger : (
|
||||
<More size={16} color={'black'} className="rotate-90" />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{renderDropdown()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RowActionsDropdown
|
||||
@@ -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 = <T extends RowDataType>({
|
||||
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<T>): React.ReactElement => {
|
||||
|
||||
const [selectedRows, setSelectedRows] = useState<T[]>([]);
|
||||
|
||||
const getRowClassName = (item: T, index: number): string => {
|
||||
if (typeof rowClassName === 'function') {
|
||||
return rowClassName(item, index);
|
||||
}
|
||||
return rowClassName;
|
||||
};
|
||||
|
||||
const getCellClassName = (column: ColumnType<T>): 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 (
|
||||
<div className={`bg-white ${roundedClass} overflow-hidden`}>
|
||||
{Array.from({ length: emptyRowsCount }).map((_, index) => (
|
||||
<div key={index} className={`h-[48px] bg-gray-100 animate-pulse ${index !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}></div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<div className={`bg-white ${roundedClass} py-6 md:py-8 text-center text-gray-500`}>
|
||||
{noDataMessage}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`bg-white ${roundedClass} overflow-hidden`}>
|
||||
{data.map((item, rowIndex) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`w-full min-h-[48px] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
{selectable && (
|
||||
<div
|
||||
className="flex-shrink-0 relative z-[5]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={isRowSelected(item)}
|
||||
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* محتوای اصلی - ستون اول */}
|
||||
<div className="flex items-center mb-0.5 text-sm font-medium">
|
||||
{columns[0]?.render ? columns[0].render(item, rowIndex) : (item[columns[0]?.key] as React.ReactNode)}
|
||||
</div>
|
||||
|
||||
{/* اطلاعات اضافی - سه ستون آخر در یک خط */}
|
||||
{columns.length > 1 && (
|
||||
<div className="flex items-center gap-3 text-xs text-gray-500">
|
||||
{columns.slice(1).map((col) => (
|
||||
<div key={col.key} className="truncate">
|
||||
{col.render ? col.render(item, rowIndex) : (item[col.key] as React.ReactNode)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{rowActions && (
|
||||
<div
|
||||
className="flex-shrink-0 relative z-[5]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<RowActionsDropdown actions={rowActions(item)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderTableBody = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Fragment>
|
||||
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data || data.length === 0) {
|
||||
return (
|
||||
<tr>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
return (data || []).map((item, rowIndex) => (
|
||||
<tr
|
||||
key={item.id}
|
||||
className={`w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
|
||||
onClick={() => handleRowClick(item)}
|
||||
>
|
||||
{selectable && (
|
||||
<td
|
||||
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
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 }}
|
||||
>
|
||||
{col.render ? col.render(item, rowIndex) : 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]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<RowActionsDropdown actions={rowActions(item)} />
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
));
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
if (!showHeader || actionsPosition === 'header-replace') return null;
|
||||
|
||||
return (
|
||||
<thead className={`h-[60px] md:h-[69px] bg-white text-sm text-[#8C90A3] ${headerClassName}`}>
|
||||
<tr>
|
||||
{selectable && (
|
||||
<th
|
||||
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5] first:rounded-tr-2xl md:first:rounded-tr-3xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={(data?.length || 0) > 0 && selectedRows.length === (data?.length || 0)}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</th>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
<Td key={col.key} text={col.title} />
|
||||
))}
|
||||
{rowActions && (
|
||||
<th className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 rounded-tl-2xl md:rounded-tl-3xl"></th>
|
||||
)}
|
||||
</tr>
|
||||
</thead >
|
||||
);
|
||||
};
|
||||
|
||||
const renderActions = () => {
|
||||
if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null;
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
// اگر header نشان داده نمیشود، از Gmail-style layout فقط در موبایل استفاده کن
|
||||
if (!showHeader) {
|
||||
return (
|
||||
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
|
||||
{/* نسخه موبایل - Gmail style */}
|
||||
<div className={`md:hidden`}>
|
||||
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
|
||||
<div className="h-[64px] bg-white flex items-center px-4 rounded-t-2xl border-b border-[#EAEDF5]">
|
||||
<div className='flex items-center gap-2'>
|
||||
{actions}
|
||||
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{renderGmailStyleRows()}
|
||||
</div>
|
||||
|
||||
{/* نسخه دسکتاپ - Table معمولی */}
|
||||
<div className={`hidden md:block`}>
|
||||
{(actionsPosition === 'top' || actionsPosition === 'above-header') && (
|
||||
<div className="h-[74px] bg-white flex items-center px-6 rounded-t-3xl">
|
||||
<div className='flex items-center gap-4'>
|
||||
{actions}
|
||||
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className={`overflow-x-auto overflow-hidden ${(actionsPosition === 'top' || actionsPosition === 'above-header') && (actions || (selectable && selectedRows.length > 0 && selectedActions)) ? 'rounded-b-3xl' : 'rounded-3xl'} bg-white`}>
|
||||
<table className="w-full text-sm border-collapse min-w-[600px]">
|
||||
<tbody>
|
||||
{renderTableBody()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pagination && (
|
||||
<Pagination
|
||||
currentPage={pagination.currentPage}
|
||||
totalPages={pagination.totalPages}
|
||||
onPageChange={pagination.onPageChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
|
||||
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
|
||||
|
||||
<div className={`overflow-x-auto overflow-hidden ${showHeader && actionsPosition !== 'header-replace' ? 'rounded-2xl md:rounded-3xl' : 'rounded-b-2xl md:rounded-b-3xl'} bg-white`}>
|
||||
<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;
|
||||
|
||||
/*
|
||||
مثال استفاده با 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
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
*/
|
||||
@@ -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: Props) => {
|
||||
return (
|
||||
<td className={`px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${props.className}`} style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
|
||||
{
|
||||
props.text ?
|
||||
props.text
|
||||
:
|
||||
props.children
|
||||
}
|
||||
</td>
|
||||
)
|
||||
}
|
||||
|
||||
export default Td
|
||||
@@ -0,0 +1,47 @@
|
||||
export interface RowDataType {
|
||||
id: string | number;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface ColumnType<T extends RowDataType> {
|
||||
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<T extends RowDataType> {
|
||||
columns: ColumnType<T>[];
|
||||
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;
|
||||
}
|
||||
@@ -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<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={clx(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={clx("flex items-center justify-center text-current")}
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="20,6 9,17 4,12" />
|
||||
</svg>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
@@ -45,3 +45,7 @@ textarea::placeholder {
|
||||
--color-border: #f5f7fc;
|
||||
--color-description: #c3c7dd;
|
||||
}
|
||||
|
||||
tbody tr:nth-child(odd) {
|
||||
background-color: #f5f7fc;
|
||||
}
|
||||
|
||||
@@ -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 = () => {
|
||||
<div className='flex-1'>
|
||||
<Stats />
|
||||
<Services />
|
||||
<Orders />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{order.number}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// کامپوننت رندر عنوان سفارش
|
||||
export const OrderTitleRenderer: FC<{ order: Order }> = ({ order }) => {
|
||||
return (
|
||||
<span className="text-sm font-medium text-gray-900">
|
||||
{order.title}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// کامپوننت رندر تاریخ
|
||||
export const OrderDateRenderer: FC<{ order: Order }> = ({ order }) => {
|
||||
return (
|
||||
<div className="text-xs text-gray-500">
|
||||
<div className="font-medium">
|
||||
({order.creationDate.gregorian})
|
||||
</div>
|
||||
<div>
|
||||
{order.creationDate.persian}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// کامپوننت رندر وضعیت سفارش
|
||||
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 (
|
||||
<span className={`text-sm font-medium ${config.color}`}>
|
||||
{order.status}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
// کامپوننت رندر پیامها
|
||||
export const OrderMessagesRenderer: FC<{ order: Order }> = ({ order }) => {
|
||||
const hasUnreadMessages = order.unreadMessages > 0;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<StatusCircle color={hasUnreadMessages ? '#EF4444' : '#3B82F6'} />
|
||||
<span className="text-xs text-gray-500">
|
||||
{order.unreadMessages} پیام خوانده نشده
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// کامپوننت رندر آیکون مشاهده
|
||||
export const OrderViewRenderer: FC<{ order: Order }> = ({ order }) => {
|
||||
return (
|
||||
<button
|
||||
className="p-1 hover:bg-gray-100 rounded-full transition-colors"
|
||||
onClick={() => {
|
||||
// اینجا میتوانید منطق مشاهده جزئیات را اضافه کنید
|
||||
console.log('View order:', order.id);
|
||||
}}
|
||||
>
|
||||
<Eye color='black' size={16} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
@@ -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<Order>[] = [
|
||||
{
|
||||
title: 'شماره',
|
||||
key: 'number',
|
||||
render: (order) => <OrderNumberRenderer order={order} />
|
||||
},
|
||||
{
|
||||
title: 'عنوان سفارش',
|
||||
key: 'title',
|
||||
render: (order) => <OrderTitleRenderer order={order} />
|
||||
},
|
||||
{
|
||||
title: 'تاریخ ایجاد',
|
||||
key: 'creationDate',
|
||||
render: (order) => <OrderDateRenderer order={order} />
|
||||
},
|
||||
{
|
||||
title: 'وضعیت سفارش',
|
||||
key: 'status',
|
||||
render: (order) => <OrderStatusRenderer order={order} />
|
||||
},
|
||||
{
|
||||
title: 'پیام ها',
|
||||
key: 'unreadMessages',
|
||||
render: (order) => <OrderMessagesRenderer order={order} />
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'actions',
|
||||
render: (order) => <OrderViewRenderer order={order} />,
|
||||
width: '60px'
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className='mt-6 bg-white p-6 rounded-3xl'>
|
||||
<h4 className='text-lg font-light'>
|
||||
سفارش های در حال انجام
|
||||
</h4>
|
||||
|
||||
<Table<Order>
|
||||
columns={columns}
|
||||
data={ordersData}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Orders
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user