@imksh/create 1.0.28 → 1.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/package.json +1 -1
  2. package/src/utils/askTemplate.js +2 -1
  3. package/templates/layers/electron/base/electron/main.js +262 -0
  4. package/templates/layers/electron/base/electron/preload.js +37 -0
  5. package/templates/layers/electron/base/electron-builder.json5 +68 -0
  6. package/templates/layers/electron/javascript/_gitignore +38 -0
  7. package/templates/layers/electron/javascript/package.json +49 -0
  8. package/templates/layers/electron/javascript/src/components/layout/SidebarLayout.jsx +43 -0
  9. package/templates/layers/electron/javascript/src/components/ui/TitleBar.jsx +130 -0
  10. package/templates/layers/electron/javascript/src/config/appConfig.js +51 -0
  11. package/templates/layers/electron/javascript/src/hooks/useElectron.js +68 -0
  12. package/templates/layers/electron/javascript/src/main.jsx +17 -0
  13. package/templates/layers/electron/javascript/src/store/useUiStore.js +59 -0
  14. package/templates/layers/electron/javascript/vite.config.js +29 -0
  15. package/templates/layers/electron/typescript/_gitignore +38 -0
  16. package/templates/layers/electron/typescript/electron/main.ts +213 -0
  17. package/templates/layers/electron/typescript/electron/preload.ts +52 -0
  18. package/templates/layers/electron/typescript/package.json +52 -0
  19. package/templates/layers/electron/typescript/src/components/layout/SidebarLayout.tsx +42 -0
  20. package/templates/layers/electron/typescript/src/components/ui/TitleBar.tsx +120 -0
  21. package/templates/layers/electron/typescript/src/config/appConfig.ts +52 -0
  22. package/templates/layers/electron/typescript/src/hooks/useElectron.ts +68 -0
  23. package/templates/layers/electron/typescript/src/main.tsx +17 -0
  24. package/templates/layers/electron/typescript/src/store/useUiStore.ts +97 -0
  25. package/templates/layers/electron/typescript/src/vite-env.d.ts +13 -0
  26. package/templates/layers/electron/typescript/tsconfig.json +25 -0
  27. package/templates/layers/electron/typescript/tsconfig.node.json +11 -0
  28. package/templates/layers/electron/typescript/vite.config.ts +29 -0
  29. package/templates/layers/next/javascript/src/components/common/Breadcrumbs.jsx +44 -6
  30. package/templates/layers/next/javascript/src/components/common/Loading.jsx +1 -0
  31. package/templates/layers/next/javascript/src/components/common/LottieIcon.jsx +1 -0
  32. package/templates/layers/next/javascript/src/components/common/Pagination.jsx +170 -0
  33. package/templates/layers/next/javascript/src/components/common/SidebarModal.jsx +180 -0
  34. package/templates/layers/next/javascript/src/components/common/StatsCard.jsx +168 -0
  35. package/templates/layers/next/javascript/src/components/common/Table.jsx +191 -41
  36. package/templates/layers/next/javascript/src/components/common/Tabs.jsx +187 -0
  37. package/templates/layers/next/javascript/src/components/common/ToggleSwitch.jsx +4 -3
  38. package/templates/layers/next/javascript/src/components/layout/ProtectedRoute.jsx +2 -1
  39. package/templates/layers/next/javascript/src/components/layout/PublicLayout.jsx +3 -6
  40. package/templates/layers/next/javascript/src/components/layout/SidebarLayout.jsx +4 -17
  41. package/templates/layers/next/javascript/src/components/modals/ConfirmModal.jsx +4 -12
  42. package/templates/layers/next/javascript/src/components/modals/Modal.jsx +50 -19
  43. package/templates/layers/next/javascript/src/components/ui/Input.jsx +50 -0
  44. package/templates/layers/next/{typescript/src/components/ui/sidebar.tsx → javascript/src/components/ui/Sidebar.jsx} +65 -53
  45. package/templates/layers/next/javascript/src/components/ui/SidebarHeader.jsx +11 -6
  46. package/templates/layers/next/javascript/src/config/appConfig.js +4 -4
  47. package/templates/layers/next/javascript/src/hooks/useWindowSize.js +2 -3
  48. package/templates/layers/next/javascript/src/store/useUiStore.js +9 -4
  49. package/templates/layers/next/typescript/src/components/common/Breadcrumbs.tsx +57 -8
  50. package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -0
  51. package/templates/layers/next/typescript/src/components/common/Pagination.tsx +186 -0
  52. package/templates/layers/next/typescript/src/components/common/SidebarModal.tsx +200 -0
  53. package/templates/layers/next/typescript/src/components/common/StatsCard.tsx +232 -0
  54. package/templates/layers/next/typescript/src/components/common/Table.tsx +201 -35
  55. package/templates/layers/next/typescript/src/components/common/Tabs.tsx +190 -0
  56. package/templates/layers/next/typescript/src/components/common/ToggleSwitch.tsx +12 -8
  57. package/templates/layers/next/typescript/src/components/layout/PublicLayout.tsx +8 -6
  58. package/templates/layers/next/typescript/src/components/layout/SidebarLayout.tsx +8 -17
  59. package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +1 -0
  60. package/templates/layers/next/typescript/src/components/modals/Modal.tsx +67 -21
  61. package/templates/layers/next/typescript/src/components/ui/Button.tsx +1 -0
  62. package/templates/layers/next/typescript/src/components/ui/Input.tsx +40 -60
  63. package/templates/layers/next/{javascript/src/components/ui/sidebar.jsx → typescript/src/components/ui/Sidebar.tsx} +77 -44
  64. package/templates/layers/next/typescript/src/components/ui/SidebarHeader.tsx +16 -10
  65. package/templates/layers/next/typescript/src/config/appConfig.ts +4 -4
  66. package/templates/layers/next/typescript/src/hooks/useWindowSize.ts +2 -3
  67. package/templates/layers/next/typescript/src/store/useUiStore.ts +14 -4
  68. package/templates/layers/node/javascript/package.json +2 -1
  69. package/templates/layers/node/javascript/src/controllers/push.controller.js +35 -14
  70. package/templates/layers/node/javascript/src/models/pushSubscription.model.js +5 -0
  71. package/templates/layers/node/javascript/src/utils/pushHelper.js +59 -12
  72. package/templates/layers/node/typescript/package-lock.json +3795 -0
  73. package/templates/layers/node/typescript/package.json +3 -2
  74. package/templates/layers/node/typescript/src/controllers/push.controller.ts +35 -14
  75. package/templates/layers/node/typescript/src/models/pushSubscription.model.ts +6 -0
  76. package/templates/layers/node/typescript/src/services/auth.service.ts +3 -3
  77. package/templates/layers/node/typescript/src/utils/pushHelper.ts +66 -12
  78. package/templates/layers/node-prisma/javascript/package.json +6 -3
  79. package/templates/layers/node-prisma/javascript/prisma/schema.prisma +20 -9
  80. package/templates/layers/node-prisma/javascript/prisma.config.js +12 -0
  81. package/templates/layers/node-prisma/javascript/src/config/{db.js → prisma.js} +9 -1
  82. package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +58 -19
  83. package/templates/layers/node-prisma/javascript/src/utils/pushHelper.js +62 -14
  84. package/templates/layers/node-prisma/typescript/package-lock.json +4808 -0
  85. package/templates/layers/node-prisma/typescript/package.json +8 -4
  86. package/templates/layers/node-prisma/typescript/prisma/schema.prisma +20 -9
  87. package/templates/layers/node-prisma/typescript/prisma.config.ts +12 -0
  88. package/templates/layers/node-prisma/typescript/src/config/{db.ts → prisma.ts} +9 -1
  89. package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -22
  90. package/templates/layers/node-prisma/typescript/src/index.ts +1 -1
  91. package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +15 -5
  92. package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +134 -128
  93. package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +69 -14
  94. package/templates/layers/prisma/typescript/prisma/schema.prisma +35 -10
  95. package/templates/layers/react/javascript/src/components/common/Breadcrumbs.jsx +62 -58
  96. package/templates/layers/react/javascript/src/components/common/Pagination.jsx +164 -76
  97. package/templates/layers/react/javascript/src/components/common/SidebarModal.jsx +179 -0
  98. package/templates/layers/react/javascript/src/components/common/StatsCard.jsx +167 -0
  99. package/templates/layers/react/javascript/src/components/common/Table.jsx +248 -132
  100. package/templates/layers/react/javascript/src/components/common/Tabs.jsx +164 -112
  101. package/templates/layers/react/javascript/src/components/common/ToggleSwitch.jsx +3 -3
  102. package/templates/layers/react/javascript/src/components/layout/SidebarLayout.jsx +4 -12
  103. package/templates/layers/react/javascript/src/components/modals/Modal.jsx +53 -18
  104. package/templates/layers/react/javascript/src/components/ui/Input.jsx +49 -0
  105. package/templates/layers/react/javascript/src/components/ui/Sidebar.jsx +215 -0
  106. package/templates/layers/react/javascript/src/components/ui/SidebarHeader.jsx +25 -30
  107. package/templates/layers/react/javascript/src/config/appConfig.js +3 -3
  108. package/templates/layers/react/javascript/src/hooks/useWindowSize.js +2 -2
  109. package/templates/layers/react/typescript/src/components/common/Breadcrumbs.tsx +32 -6
  110. package/templates/layers/react/typescript/src/components/common/Pagination.tsx +151 -57
  111. package/templates/layers/react/typescript/src/components/common/SidebarModal.tsx +199 -0
  112. package/templates/layers/react/typescript/src/components/common/StatsCard.tsx +231 -0
  113. package/templates/layers/react/typescript/src/components/common/Table.tsx +197 -34
  114. package/templates/layers/react/typescript/src/components/common/Tabs.tsx +56 -12
  115. package/templates/layers/react/typescript/src/components/common/ToggleSwitch.tsx +15 -3
  116. package/templates/layers/react/typescript/src/components/layout/SidebarLayout.tsx +3 -6
  117. package/templates/layers/react/typescript/src/components/modals/Modal.tsx +151 -0
  118. package/templates/layers/react/typescript/src/components/ui/Input.tsx +58 -0
  119. package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +116 -53
  120. package/templates/layers/react/typescript/src/components/ui/SidebarHeader.tsx +5 -4
  121. package/templates/layers/react/typescript/src/config/appConfig.ts +3 -3
  122. package/templates/layers/react/typescript/src/hooks/useWindowSize.ts +2 -3
  123. package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +2 -1
  124. package/templates/layers/react-native/base/src/config/appConfig.ts +44 -0
  125. package/templates/presets/electron/preset.json +9 -0
  126. package/templates/presets/electron-ts/preset.json +9 -0
  127. package/templates/layers/react/javascript/src/components/ui/sidebar.jsx +0 -169
@@ -7,17 +7,42 @@ datasource db {
7
7
  url = env("DATABASE_URL")
8
8
  }
9
9
 
10
+ enum AttachmentType {
11
+ IMAGE
12
+ DOCUMENT
13
+ VIDEO
14
+ PDF
15
+ OTHER
16
+ }
17
+
18
+ model Attachment {
19
+ id String @id @default(uuid())
20
+ publicId String
21
+ url String
22
+ name String
23
+ type AttachmentType
24
+
25
+ createdAt DateTime @default(now())
26
+ updatedAt DateTime @updatedAt
27
+ deletedAt DateTime?
28
+
29
+ // Back-relations
30
+ avatar User? @relation("avatar")
31
+ }
32
+
10
33
  model User {
11
- id String @id @default(uuid())
12
- email String @unique
13
- name String
14
- phone String?
15
- password String
16
- role String @default("user")
17
- avatarUrl String?
18
- avatarPublicId String?
19
- createdAt DateTime @default(now())
20
- updatedAt DateTime @updatedAt
34
+ id String @id @default(uuid())
35
+ email String @unique
36
+ name String
37
+ phone String?
38
+ password String
39
+ role String @default("user")
40
+
41
+ avatarId String? @unique
42
+ avatar Attachment? @relation("avatar", fields: [avatarId], references: [id])
43
+
44
+ createdAt DateTime @default(now())
45
+ updatedAt DateTime @updatedAt
21
46
  pushSubscriptions PushSubscription[]
22
47
  }
23
48
 
@@ -1,60 +1,64 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
1
  import React from "react";
3
- import { Link } from "react-router-dom";
4
- import { ChevronRight } from "lucide-react";
5
- const Breadcrumbs = ({ items }) => {
6
- return /* @__PURE__ */ jsx("div", {
7
- className:
8
- "flex items-center gap-1.5 text-sm font-bold text-base-content/50 flex-wrap",
9
- children: items.map((item, idx) => {
10
- const isLast = idx === items.length - 1;
11
- return /* @__PURE__ */ jsxs(
12
- React.Fragment,
13
- {
14
- children: [
15
- item.path && !isLast
16
- ? /* @__PURE__ */ jsx(Link, {
17
- to: item.path,
18
- onClick: item.onClick,
19
- className:
20
- "hover:text-primary transition-colors hidden sm:flex items-center gap-1",
21
- children: item.label,
22
- })
23
- : item.onClick && !isLast
24
- ? /* @__PURE__ */ jsx("button", {
25
- type: "button",
26
- onClick: item.onClick,
27
- className:
28
- "hover:text-primary transition-colors hidden sm:flex items-center gap-1",
29
- children: item.label,
30
- })
31
- : /* @__PURE__ */ jsxs("div", {
32
- className: "flex flex-col",
33
- children: [
34
- /* @__PURE__ */ jsx("span", {
35
- className: isLast
36
- ? "text-lg md:text-xl font-bold leading-tight text-base-content tracking-tight truncate max-w-[200px] md:max-w-none"
37
- : "",
38
- children: item.label,
39
- }),
40
- item.subtitle &&
41
- /* @__PURE__ */ jsx("span", {
42
- className:
43
- "text-xs text-base-content/60 font-normal truncate",
44
- children: item.subtitle,
45
- }),
46
- ],
47
- }),
48
- !isLast &&
49
- /* @__PURE__ */ jsx(ChevronRight, {
50
- size: 14,
51
- className: "text-base-content/30 shrink-0 hidden sm:block",
52
- }),
53
- ],
54
- },
55
- idx,
56
- );
57
- }),
58
- });
2
+ import { Link, useNavigate } from "react-router-dom";
3
+ import { ArrowLeft, ChevronRight } from "lucide-react";
4
+
5
+ export const Breadcrumbs = ({ items }) => {
6
+ const navigation = useNavigate();
7
+ return (
8
+ <div className="flex items-center gap-1.5 text-sm font-bold text-base-content/50 flex-wrap">
9
+ <button
10
+ className="btn btn-primary btn-soft btn-square btn-sm mr-1 hidden md:flex"
11
+ onClick={() => navigation(-1)}
12
+ >
13
+ <ArrowLeft size={18} />
14
+ </button>
15
+ {items.map((item, idx) => {
16
+ const isLast = idx === items.length - 1;
17
+ return (
18
+ <React.Fragment key={idx}>
19
+ {item.path && !isLast ? (
20
+ <Link
21
+ to={item.path}
22
+ onClick={item.onClick}
23
+ className="hover:text-primary transition-colors hidden sm:flex items-center gap-1"
24
+ >
25
+ {item.label}
26
+ </Link>
27
+ ) : item.onClick && !isLast ? (
28
+ <button
29
+ type="button"
30
+ onClick={item.onClick}
31
+ className="hover:text-primary transition-colors hidden sm:flex items-center gap-1"
32
+ >
33
+ {item.label}
34
+ </button>
35
+ ) : (
36
+ <div className="flex flex-col">
37
+ <span
38
+ className={
39
+ isLast
40
+ ? "text-lg md:text-xl font-bold leading-tight text-base-content tracking-tight truncate max-w-[200px] md:max-w-none"
41
+ : ""
42
+ }
43
+ >
44
+ {item.label}
45
+ </span>
46
+ {item.subtitle && (
47
+ <span className="text-xs text-base-content/60 font-normal truncate">
48
+ {item.subtitle}
49
+ </span>
50
+ )}
51
+ </div>
52
+ )}
53
+ {!isLast && (
54
+ <ChevronRight
55
+ size={14}
56
+ className="text-base-content/30 shrink-0 hidden sm:block"
57
+ />
58
+ )}
59
+ </React.Fragment>
60
+ );
61
+ })}
62
+ </div>
63
+ );
59
64
  };
60
- export { Breadcrumbs };
@@ -1,81 +1,169 @@
1
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
1
+ import React from "react";
2
2
  import { ChevronLeft, ChevronRight } from "lucide-react";
3
- const Pagination = ({ currentPage, totalPages, onPageChange }) => {
3
+
4
+ export const Pagination = ({ currentPage, totalPages, onPageChange }) => {
4
5
  if (totalPages <= 1) return null;
5
- const maxVisiblePages = 5;
6
- let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
7
- const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
8
- if (endPage - startPage + 1 < maxVisiblePages) {
9
- startPage = Math.max(1, endPage - maxVisiblePages + 1);
6
+
7
+ // Desktop pagination: 5 pages with outer bounds & ellipsis
8
+ const maxDesktopPages = 5;
9
+ let startDesktopPage = Math.max(
10
+ 1,
11
+ currentPage - Math.floor(maxDesktopPages / 2),
12
+ );
13
+ let endDesktopPage = Math.min(
14
+ totalPages,
15
+ startDesktopPage + maxDesktopPages - 1,
16
+ );
17
+ if (endDesktopPage - startDesktopPage + 1 < maxDesktopPages) {
18
+ startDesktopPage = Math.max(1, endDesktopPage - maxDesktopPages + 1);
10
19
  }
11
- const pages = [];
12
- for (let i = startPage; i <= endPage; i++) {
13
- pages.push(i);
20
+ const desktopPages = [];
21
+ for (let i = startDesktopPage; i <= endDesktopPage; i++) {
22
+ desktopPages.push(i);
14
23
  }
15
- return /* @__PURE__ */ jsxs("div", {
16
- className:
17
- "flex items-center justify-center gap-2 mt-10 p-2 rounded-2xl bg-base-100/50 backdrop-blur-sm border border-base-200/50 w-fit mx-auto shadow-sm",
18
- children: [
19
- /* @__PURE__ */ jsx("button", {
20
- onClick: () => onPageChange(currentPage - 1),
21
- disabled: currentPage === 1,
22
- className:
23
- "flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-200 text-base-content/70 hover:bg-base-content/10 hover:text-base-content disabled:opacity-30 disabled:hover:bg-transparent",
24
- "aria-label": "Previous Page",
25
- children: /* @__PURE__ */ jsx(ChevronLeft, { className: "w-5 h-5" }),
26
- }),
27
- startPage > 1 &&
28
- /* @__PURE__ */ jsxs(Fragment, {
29
- children: [
30
- /* @__PURE__ */ jsx("button", {
31
- onClick: () => onPageChange(1),
32
- className:
33
- "flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-200 font-medium text-base-content/70 hover:bg-base-content/10 hover:text-base-content",
34
- children: "1",
35
- }),
36
- startPage > 2 &&
37
- /* @__PURE__ */ jsx("span", {
38
- className: "px-1 text-base-content/40",
39
- children: "...",
40
- }),
41
- ],
42
- }),
43
- pages.map((page) =>
44
- /* @__PURE__ */ jsx(
45
- "button",
46
- {
47
- onClick: () => onPageChange(page),
48
- className: `flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-300 font-semibold ${currentPage === page ? "bg-primary text-primary-content shadow-lg shadow-primary/30 scale-105" : "text-base-content/70 hover:bg-base-content/10 hover:text-base-content"}`,
49
- children: page,
50
- },
51
- page,
52
- ),
53
- ),
54
- endPage < totalPages &&
55
- /* @__PURE__ */ jsxs(Fragment, {
56
- children: [
57
- endPage < totalPages - 1 &&
58
- /* @__PURE__ */ jsx("span", {
59
- className: "px-1 text-base-content/40",
60
- children: "...",
61
- }),
62
- /* @__PURE__ */ jsx("button", {
63
- onClick: () => onPageChange(totalPages),
64
- className:
65
- "flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-200 font-medium text-base-content/70 hover:bg-base-content/10 hover:text-base-content",
66
- children: totalPages,
67
- }),
68
- ],
69
- }),
70
- /* @__PURE__ */ jsx("button", {
71
- onClick: () => onPageChange(currentPage + 1),
72
- disabled: currentPage === totalPages,
73
- className:
74
- "flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-200 text-base-content/70 hover:bg-base-content/10 hover:text-base-content disabled:opacity-30 disabled:hover:bg-transparent",
75
- "aria-label": "Next Page",
76
- children: /* @__PURE__ */ jsx(ChevronRight, { className: "w-5 h-5" }),
77
- }),
78
- ],
79
- });
24
+
25
+ // Small screen pagination: exactly 3 page circles
26
+ const maxMobilePages = 3;
27
+ let startMobilePage = Math.max(
28
+ 1,
29
+ currentPage - Math.floor(maxMobilePages / 2),
30
+ );
31
+ let endMobilePage = Math.min(
32
+ totalPages,
33
+ startMobilePage + maxMobilePages - 1,
34
+ );
35
+ if (endMobilePage - startMobilePage + 1 < maxMobilePages) {
36
+ startMobilePage = Math.max(1, endMobilePage - maxMobilePages + 1);
37
+ }
38
+ const mobilePages = [];
39
+ for (let i = startMobilePage; i <= endMobilePage; i++) {
40
+ mobilePages.push(i);
41
+ }
42
+
43
+ const baseBtnClass =
44
+ "relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1.5 sm:px-2 text-xs sm:text-sm font-medium transition-all duration-200 rounded-full bg-base-100 hover:bg-base-200 text-base-content/70 hover:text-base-content focus:z-10 focus:outline-none focus:ring-2 focus:ring-primary/50 shadow-sm";
45
+ const activeBtnClass =
46
+ "relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1.5 sm:px-2 text-xs sm:text-sm font-bold transition-all duration-200 rounded-full border border-primary bg-primary text-primary-content z-10 focus:outline-none focus:ring-2 focus:ring-primary/50 shadow-sm shadow-primary/20";
47
+ const disabledBtnClass =
48
+ "relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1.5 sm:px-2 text-xs sm:text-sm font-medium transition-all duration-200 rounded-full bg-base-100 text-base-content/30 cursor-not-allowed shadow-sm";
49
+ const ellipsisClass =
50
+ "relative inline-flex items-center justify-center min-w-8 h-8 sm:min-w-9 sm:h-9 px-1 sm:px-2 text-xs sm:text-sm font-medium text-base-content/40 cursor-default";
51
+
52
+ return (
53
+ <>
54
+ {/* Mobile / Small Screen: 3 page circles + ellipses + boundary pages */}
55
+ <div className="flex sm:hidden items-center justify-center gap-1">
56
+ <button
57
+ onClick={() => onPageChange(currentPage - 1)}
58
+ disabled={currentPage === 1}
59
+ className={currentPage === 1 ? disabledBtnClass : baseBtnClass}
60
+ aria-label="Previous Page"
61
+ >
62
+ <ChevronLeft className="w-4 h-4" />
63
+ </button>
64
+
65
+ {startMobilePage > 1 && (
66
+ <>
67
+ <button onClick={() => onPageChange(1)} className={baseBtnClass}>
68
+ 1
69
+ </button>
70
+ {startMobilePage > 2 && <span className={ellipsisClass}>...</span>}
71
+ </>
72
+ )}
73
+
74
+ {mobilePages.map((page) => (
75
+ <button
76
+ key={`mobile-${page}`}
77
+ onClick={() => onPageChange(page)}
78
+ className={currentPage === page ? activeBtnClass : baseBtnClass}
79
+ >
80
+ {page}
81
+ </button>
82
+ ))}
83
+
84
+ {endMobilePage < totalPages && (
85
+ <>
86
+ {endMobilePage < totalPages - 1 && (
87
+ <span className={ellipsisClass}>...</span>
88
+ )}
89
+ <button
90
+ onClick={() => onPageChange(totalPages)}
91
+ className={baseBtnClass}
92
+ >
93
+ {totalPages}
94
+ </button>
95
+ </>
96
+ )}
97
+
98
+ <button
99
+ onClick={() => onPageChange(currentPage + 1)}
100
+ disabled={currentPage === totalPages}
101
+ className={
102
+ currentPage === totalPages ? disabledBtnClass : baseBtnClass
103
+ }
104
+ aria-label="Next Page"
105
+ >
106
+ <ChevronRight className="w-4 h-4" />
107
+ </button>
108
+ </div>
109
+
110
+ {/* Desktop Screen: 5 page circles + ellipses + boundary pages */}
111
+ <div className="hidden sm:flex items-center justify-center gap-1.5">
112
+ <button
113
+ onClick={() => onPageChange(currentPage - 1)}
114
+ disabled={currentPage === 1}
115
+ className={currentPage === 1 ? disabledBtnClass : baseBtnClass}
116
+ aria-label="Previous Page"
117
+ >
118
+ <ChevronLeft className="w-4 h-4" />
119
+ </button>
120
+
121
+ {startDesktopPage > 1 && (
122
+ <>
123
+ <button onClick={() => onPageChange(1)} className={baseBtnClass}>
124
+ 1
125
+ </button>
126
+ {startDesktopPage > 2 && <span className={ellipsisClass}>...</span>}
127
+ </>
128
+ )}
129
+
130
+ {desktopPages.map((page) => (
131
+ <button
132
+ key={`desktop-${page}`}
133
+ onClick={() => onPageChange(page)}
134
+ className={currentPage === page ? activeBtnClass : baseBtnClass}
135
+ >
136
+ {page}
137
+ </button>
138
+ ))}
139
+
140
+ {endDesktopPage < totalPages && (
141
+ <>
142
+ {endDesktopPage < totalPages - 1 && (
143
+ <span className={ellipsisClass}>...</span>
144
+ )}
145
+ <button
146
+ onClick={() => onPageChange(totalPages)}
147
+ className={baseBtnClass}
148
+ >
149
+ {totalPages}
150
+ </button>
151
+ </>
152
+ )}
153
+
154
+ <button
155
+ onClick={() => onPageChange(currentPage + 1)}
156
+ disabled={currentPage === totalPages}
157
+ className={
158
+ currentPage === totalPages ? disabledBtnClass : baseBtnClass
159
+ }
160
+ aria-label="Next Page"
161
+ >
162
+ <ChevronRight className="w-4 h-4" />
163
+ </button>
164
+ </div>
165
+ </>
166
+ );
80
167
  };
81
- export { Pagination };
168
+
169
+ export default Pagination;
@@ -0,0 +1,179 @@
1
+ import React, { useEffect } from "react";
2
+ import { createPortal } from "react-dom";
3
+ import { motion, AnimatePresence } from "framer-motion";
4
+ import { IoClose } from "react-icons/io5";
5
+ import { useUiStore } from "../../store/useUiStore";
6
+
7
+ export const SidebarModal = ({
8
+ isOpen,
9
+ onClose,
10
+ side = "right",
11
+ title,
12
+ description,
13
+ children,
14
+ width = "w-full sm:w-[420px]",
15
+ showCloseButton = true,
16
+ closeOnBackdrop = true,
17
+ closeOnEsc = true,
18
+ className = "",
19
+ header,
20
+ footer,
21
+ bodyClassName = "p-4 space-y-4",
22
+ hasBackdrop = true,
23
+ }) => {
24
+ const setModalOpen = useUiStore((state) => state.setModalOpen);
25
+
26
+ // Sync with global UI store modal state for scroll locking
27
+ useEffect(() => {
28
+ if (isOpen) {
29
+ setModalOpen(true);
30
+ return () => {
31
+ setModalOpen(false);
32
+ };
33
+ }
34
+ }, [isOpen, setModalOpen]);
35
+
36
+ // Handle ESC key
37
+ useEffect(() => {
38
+ if (!isOpen || !closeOnEsc) return;
39
+
40
+ const handleKeyDown = (e) => {
41
+ if (e.key === "Escape") {
42
+ onClose();
43
+ }
44
+ };
45
+
46
+ window.addEventListener("keydown", handleKeyDown);
47
+ return () => window.removeEventListener("keydown", handleKeyDown);
48
+ }, [isOpen, closeOnEsc, onClose]);
49
+
50
+ const slideVariants = {
51
+ hidden: {
52
+ x: side === "right" ? "100%" : "-100%",
53
+ opacity: 0.7,
54
+ },
55
+ visible: {
56
+ x: 0,
57
+ opacity: 1,
58
+ transition: {
59
+ type: "spring",
60
+ damping: 30,
61
+ stiffness: 300,
62
+ mass: 0.8,
63
+ },
64
+ },
65
+ exit: {
66
+ x: side === "right" ? "100%" : "-100%",
67
+ opacity: 0.5,
68
+ transition: {
69
+ type: "tween",
70
+ duration: 0.22,
71
+ ease: "easeInOut",
72
+ },
73
+ },
74
+ };
75
+
76
+ const backdropVariants = {
77
+ hidden: { opacity: 0 },
78
+ visible: { opacity: 1, transition: { duration: 0.2 } },
79
+ exit: { opacity: 0, transition: { duration: 0.18 } },
80
+ };
81
+
82
+ const content = (
83
+ <AnimatePresence>
84
+ {isOpen && (
85
+ <div className="fixed inset-0 z-50 overflow-hidden">
86
+ {/* Backdrop */}
87
+ {hasBackdrop && (
88
+ <motion.div
89
+ variants={backdropVariants}
90
+ initial="hidden"
91
+ animate="visible"
92
+ exit="exit"
93
+ className="fixed inset-0 bg-black/40 backdrop-blur-xs transition-opacity"
94
+ onClick={() => {
95
+ if (closeOnBackdrop) onClose();
96
+ }}
97
+ />
98
+ )}
99
+
100
+ {/* Sliding Panel */}
101
+ <div
102
+ className={`fixed inset-y-0 ${
103
+ side === "right" ? "right-0" : "left-0"
104
+ } max-w-full flex`}
105
+ >
106
+ <motion.div
107
+ variants={slideVariants}
108
+ initial="hidden"
109
+ animate="visible"
110
+ exit="exit"
111
+ className={`
112
+ relative h-full flex flex-col bg-base-100 border-base-300 shadow-2xl overflow-hidden
113
+ ${side === "right" ? "border-l" : "border-r"}
114
+ ${width}
115
+ ${className}
116
+ `}
117
+ onClick={(e) => e.stopPropagation()}
118
+ >
119
+ {/* Optional Custom Header or Default Header */}
120
+ {header ? (
121
+ <div className="shrink-0">{header}</div>
122
+ ) : title ? (
123
+ <div className="flex items-center justify-between p-4 border-b border-base-300 bg-base-200/40 shrink-0">
124
+ <div className="flex-1 min-w-0 pr-2">
125
+ <h2 className="text-base font-bold text-base-content truncate">
126
+ {title}
127
+ </h2>
128
+ {description && (
129
+ <p className="text-xs text-base-content/60 truncate mt-0.5">
130
+ {description}
131
+ </p>
132
+ )}
133
+ </div>
134
+ {showCloseButton && (
135
+ <button
136
+ type="button"
137
+ className="btn btn-circle btn-sm btn-error btn-soft text-error/70 hover:text-error hover:bg-error/20 hover:shadow-sm hover:rotate-180 transition-transform duration-200"
138
+ onClick={onClose}
139
+ title="Close"
140
+ >
141
+ <IoClose size={18} />
142
+ </button>
143
+ )}
144
+ </div>
145
+ ) : showCloseButton ? (
146
+ <button
147
+ type="button"
148
+ className="btn btn-circle btn-sm btn-error btn-soft text-error/70 hover:text-error hover:bg-error/20 hover:shadow-sm hover:rotate-180 transition-transform duration-200"
149
+ onClick={onClose}
150
+ title="Close"
151
+ >
152
+ <IoClose size={18} />
153
+ </button>
154
+ ) : null}
155
+
156
+ {/* Scrollable Body */}
157
+ <div className={`flex-1 overflow-y-auto ${bodyClassName}`}>
158
+ {children}
159
+ </div>
160
+
161
+ {/* Optional Footer */}
162
+ {footer && (
163
+ <div className="p-4 border-t border-base-300 bg-base-200/30 shrink-0">
164
+ {footer}
165
+ </div>
166
+ )}
167
+ </motion.div>
168
+ </div>
169
+ </div>
170
+ )}
171
+ </AnimatePresence>
172
+ );
173
+
174
+ return typeof document !== "undefined"
175
+ ? createPortal(content, document.body)
176
+ : null;
177
+ };
178
+
179
+ export default SidebarModal;