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",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.9",
|
"@headlessui/react": "^2.2.9",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@tanstack/react-query": "^5.90.3",
|
"@tanstack/react-query": "^5.90.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^12.0.2",
|
"swiper": "^12.0.2",
|
||||||
@@ -1115,6 +1117,222 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/@react-aria/focus": {
|
||||||
"version": "3.21.2",
|
"version": "3.21.2",
|
||||||
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz",
|
"resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.2.tgz",
|
||||||
@@ -1911,7 +2129,7 @@
|
|||||||
"version": "19.2.2",
|
"version": "19.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
||||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
@@ -4062,6 +4280,15 @@
|
|||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.9",
|
"@headlessui/react": "^2.2.9",
|
||||||
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@tailwindcss/vite": "^4.1.14",
|
"@tailwindcss/vite": "^4.1.14",
|
||||||
"@tanstack/react-query": "^5.90.3",
|
"@tanstack/react-query": "^5.90.3",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
@@ -18,6 +19,7 @@
|
|||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
"react-loading-skeleton": "^3.5.0",
|
||||||
"react-router-dom": "^7.9.4",
|
"react-router-dom": "^7.9.4",
|
||||||
"react-spinners": "^0.17.0",
|
"react-spinners": "^0.17.0",
|
||||||
"swiper": "^12.0.2",
|
"swiper": "^12.0.2",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import '@/assets/fonts/irancell/style.css'
|
import '@/assets/fonts/irancell/style.css'
|
||||||
import 'swiper/swiper-bundle.css';
|
import 'swiper/swiper-bundle.css';
|
||||||
|
import 'react-loading-skeleton/dist/skeleton.css'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
// import { getToken } from './config/func'
|
// import { getToken } from './config/func'
|
||||||
import MainRouter from './router/MainRouter'
|
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-border: #f5f7fc;
|
||||||
--color-description: #c3c7dd;
|
--color-description: #c3c7dd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(odd) {
|
||||||
|
background-color: #f5f7fc;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { type FC } from 'react'
|
import { type FC } from 'react'
|
||||||
import Stats from './components/Stats'
|
import Stats from './components/Stats'
|
||||||
import Services from './components/Services'
|
import Services from './components/Services'
|
||||||
|
import Orders from './components/Orders'
|
||||||
|
|
||||||
const Home: FC = () => {
|
const Home: FC = () => {
|
||||||
return (
|
return (
|
||||||
@@ -8,6 +9,7 @@ const Home: FC = () => {
|
|||||||
<div className='flex-1'>
|
<div className='flex-1'>
|
||||||
<Stats />
|
<Stats />
|
||||||
<Services />
|
<Services />
|
||||||
|
<Orders />
|
||||||
</div>
|
</div>
|
||||||
</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