@imksh/create 1.0.30 → 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 (102) 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/typescript/package-lock.json +2 -2
  69. package/templates/layers/node/typescript/package.json +1 -1
  70. package/templates/layers/prisma/typescript/prisma/schema.prisma +35 -10
  71. package/templates/layers/react/javascript/src/components/common/Breadcrumbs.jsx +62 -58
  72. package/templates/layers/react/javascript/src/components/common/Pagination.jsx +164 -76
  73. package/templates/layers/react/javascript/src/components/common/SidebarModal.jsx +179 -0
  74. package/templates/layers/react/javascript/src/components/common/StatsCard.jsx +167 -0
  75. package/templates/layers/react/javascript/src/components/common/Table.jsx +248 -132
  76. package/templates/layers/react/javascript/src/components/common/Tabs.jsx +164 -112
  77. package/templates/layers/react/javascript/src/components/common/ToggleSwitch.jsx +3 -3
  78. package/templates/layers/react/javascript/src/components/layout/SidebarLayout.jsx +4 -12
  79. package/templates/layers/react/javascript/src/components/modals/Modal.jsx +53 -18
  80. package/templates/layers/react/javascript/src/components/ui/Input.jsx +49 -0
  81. package/templates/layers/react/javascript/src/components/ui/Sidebar.jsx +215 -0
  82. package/templates/layers/react/javascript/src/components/ui/SidebarHeader.jsx +25 -30
  83. package/templates/layers/react/javascript/src/config/appConfig.js +3 -3
  84. package/templates/layers/react/javascript/src/hooks/useWindowSize.js +2 -2
  85. package/templates/layers/react/typescript/src/components/common/Breadcrumbs.tsx +32 -6
  86. package/templates/layers/react/typescript/src/components/common/Pagination.tsx +151 -57
  87. package/templates/layers/react/typescript/src/components/common/SidebarModal.tsx +199 -0
  88. package/templates/layers/react/typescript/src/components/common/StatsCard.tsx +231 -0
  89. package/templates/layers/react/typescript/src/components/common/Table.tsx +197 -34
  90. package/templates/layers/react/typescript/src/components/common/Tabs.tsx +56 -12
  91. package/templates/layers/react/typescript/src/components/common/ToggleSwitch.tsx +15 -3
  92. package/templates/layers/react/typescript/src/components/layout/SidebarLayout.tsx +3 -6
  93. package/templates/layers/react/typescript/src/components/modals/Modal.tsx +151 -0
  94. package/templates/layers/react/typescript/src/components/ui/Input.tsx +58 -0
  95. package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +116 -53
  96. package/templates/layers/react/typescript/src/components/ui/SidebarHeader.tsx +5 -4
  97. package/templates/layers/react/typescript/src/config/appConfig.ts +3 -3
  98. package/templates/layers/react/typescript/src/hooks/useWindowSize.ts +2 -3
  99. package/templates/layers/react-native/base/src/config/appConfig.ts +1 -1
  100. package/templates/presets/electron/preset.json +9 -0
  101. package/templates/presets/electron-ts/preset.json +9 -0
  102. package/templates/layers/react/javascript/src/components/ui/sidebar.jsx +0 -169
@@ -0,0 +1,215 @@
1
+ import { NavLink } from "react-router-dom";
2
+ import { IoClose } from "react-icons/io5";
3
+ import { useUiStore } from "../../store/useUiStore";
4
+ import { navigation } from "../../config/navigation";
5
+ import { Moon, Sun } from "lucide-react";
6
+ import appConfig from "../../config/appConfig";
7
+ import { motion, AnimatePresence } from "framer-motion";
8
+ import useWindowSize from "../../hooks/useWindowSize";
9
+ import { useAuthStore } from "../../store/useAuthStore";
10
+ import { GoSidebarCollapse, GoSidebarExpand } from "react-icons/go";
11
+ import { CiLogout } from "react-icons/ci";
12
+
13
+ const Sidebar = () => {
14
+ const { open, close, collapsed, toggleCollapsed, theme, toggleTheme } =
15
+ useUiStore();
16
+ const { logout, user } = useAuthStore();
17
+ const { width } = useWindowSize();
18
+ const isMobile = width < 1024; // lg breakpoint
19
+
20
+ const showSidebar = !isMobile || open;
21
+
22
+ return (
23
+ <>
24
+ {/* Overlay with AnimatePresence */}
25
+ <AnimatePresence>
26
+ {isMobile && open && (
27
+ <motion.div
28
+ initial={{ opacity: 0 }}
29
+ animate={{ opacity: 1 }}
30
+ exit={{ opacity: 0 }}
31
+ transition={{ duration: 0.2 }}
32
+ className="fixed inset-0 bg-base-100/40 z-40 lg:hidden"
33
+ onClick={close}
34
+ />
35
+ )}
36
+ </AnimatePresence>
37
+
38
+ {/* Sidebar with AnimatePresence */}
39
+ <AnimatePresence>
40
+ {showSidebar && (
41
+ <motion.aside
42
+ initial={isMobile ? { x: "-100%" } : false}
43
+ animate={{ x: 0 }}
44
+ exit={isMobile ? { x: "-100%" } : undefined}
45
+ transition={{ type: "tween", duration: 0.25, ease: "easeInOut" }}
46
+ className={`
47
+ fixed lg:static top-0 left-0 z-50
48
+ h-screen bg-base-200 border-r border-base-300
49
+ flex flex-col overflow-hidden
50
+ transition-[width] duration-300
51
+ ${collapsed ? "w-18" : "w-[75%] md:w-[16%] md:max-w-60"}
52
+ `}
53
+ >
54
+ {/* Header */}
55
+ <div className="h-16 border-b border-base-300 flex items-center justify-between px-5">
56
+ <div className="flex items-center gap-3">
57
+ {appConfig.app.logo ? (
58
+ <img
59
+ src={appConfig.app.logo}
60
+ alt=""
61
+ className="w-8 h-8 rounded-lg"
62
+ />
63
+ ) : (
64
+ <div className="flex items-center">
65
+ <div className="bg-primary w-8 h-8 rounded-lg flex items-center justify-center text-primary-content font-bold">
66
+ {appConfig.app.name.charAt(0)}
67
+ </div>
68
+ </div>
69
+ )}
70
+ <h1
71
+ className={`text-xl font-black text-primary whitespace-nowrap overflow-hidden ${collapsed ? "hidden" : ""}`}
72
+ >
73
+ {appConfig.app.name}
74
+ </h1>
75
+ </div>
76
+
77
+ <button
78
+ className={`btn btn-sm btn-circle btn-ghost md:hidden ${collapsed ? "hidden" : ""}`}
79
+ onClick={close}
80
+ >
81
+ <IoClose size={20} />
82
+ </button>
83
+ </div>
84
+
85
+ {/* Navigation */}
86
+ <nav
87
+ className="flex-1 overflow-y-auto p-2.5 space-y-1"
88
+ data-lenis-prevent
89
+ >
90
+ {navigation
91
+ .filter((item) => {
92
+ const orgRole = user?.orgRole;
93
+ const isUserAdmin =
94
+ user?.role === "ADMIN" ||
95
+ orgRole === "OWNER" ||
96
+ orgRole === "ADMIN";
97
+
98
+ // Check if item is hidden for this role
99
+ if (item.hiddenForRoles) {
100
+ if (
101
+ isUserAdmin &&
102
+ (item.hiddenForRoles.includes("OWNER") ||
103
+ item.hiddenForRoles.includes("ADMIN"))
104
+ ) {
105
+ return false;
106
+ }
107
+ if (orgRole && item.hiddenForRoles.includes(orgRole)) {
108
+ return false;
109
+ }
110
+ }
111
+
112
+ if (!item.allowedRoles) return true; // visible to all
113
+ // System-level ADMIN always gets full access
114
+ if (user?.role === "ADMIN") return true;
115
+ // If orgRole not yet resolved, show all items (fail open for admins)
116
+ if (!orgRole) return true;
117
+ return item.allowedRoles.includes(orgRole);
118
+ })
119
+ .map((item) => {
120
+ const Icon = item.icon;
121
+
122
+ return (
123
+ <NavLink
124
+ key={item.path}
125
+ to={item.path}
126
+ onClick={isMobile ? close : undefined}
127
+ className={({ isActive }) =>
128
+ `flex items-center gap-3 rounded-box px-4 py-3 duration-0 h-12
129
+ ${
130
+ isActive
131
+ ? "bg-primary text-primary-content"
132
+ : "hover:bg-base-300"
133
+ } ${collapsed ? "justify-center px-0" : ""}`
134
+ }
135
+ title={collapsed ? item.title : ""}
136
+ >
137
+ <Icon
138
+ size={18}
139
+ className={`shrink-0 ${collapsed && "mr-auto"}`}
140
+ />
141
+
142
+ {!collapsed && (
143
+ <span className="font-medium transition-all duration-0 whitespace-nowrap">
144
+ {item.title}
145
+ </span>
146
+ )}
147
+ </NavLink>
148
+ );
149
+ })}
150
+ </nav>
151
+
152
+ {/* Footer with Actions */}
153
+ <div
154
+ className={`p-3 border-t border-base-300/50 shrink-0 flex gap-2 ${
155
+ collapsed
156
+ ? "flex-col items-center justify-center"
157
+ : "flex-row items-center justify-between"
158
+ }`}
159
+ >
160
+ <button
161
+ onClick={() => {
162
+ logout();
163
+ close();
164
+ }}
165
+ className={`btn btn-sm btn-circle btn-soft btn-error ${collapsed ? "hidden" : "flex"}`}
166
+ title="Logout"
167
+ >
168
+ <CiLogout size={18} />
169
+ </button>
170
+
171
+ <div
172
+ className={`flex items-center gap-2 ${collapsed ? "flex-col" : ""}`}
173
+ >
174
+ <button
175
+ onClick={toggleTheme}
176
+ className="flex items-center gap-3 hover:opacity-80 transition-opacity"
177
+ title="Toggle Theme"
178
+ >
179
+ <div
180
+ className={`btn btn-sm btn-circle btn-primary btn-soft pointer-events-none ${collapsed ? "hidden" : "flex"}`}
181
+ >
182
+ {theme === "dark" ? (
183
+ <Sun size={16} className="shrink-0" />
184
+ ) : (
185
+ <Moon size={16} className="shrink-0" />
186
+ )}
187
+ </div>
188
+ {!collapsed && (
189
+ <span className="md:hidden font-medium text-sm text-base-content/80">
190
+ {theme === "dark" ? "Dark Mode" : "Light Mode"}
191
+ </span>
192
+ )}
193
+ </button>
194
+
195
+ <button
196
+ onClick={toggleCollapsed}
197
+ className="btn btn-sm btn-circle btn-outline border-none text-base-content/70 hover:text-base-content hover:bg-base-300/60 hidden md:flex"
198
+ title={collapsed ? "Expand Sidebar" : "Collapse Sidebar"}
199
+ >
200
+ {collapsed ? (
201
+ <GoSidebarExpand size={18} />
202
+ ) : (
203
+ <GoSidebarCollapse size={18} />
204
+ )}
205
+ </button>
206
+ </div>
207
+ </div>
208
+ </motion.aside>
209
+ )}
210
+ </AnimatePresence>
211
+ </>
212
+ );
213
+ };
214
+
215
+ export default Sidebar;
@@ -1,36 +1,31 @@
1
- import { jsx, jsxs } from "react/jsx-runtime";
2
1
  import { FiMenu } from "react-icons/fi";
3
2
  import { useUiStore } from "../../store/useUiStore";
3
+
4
4
  import { Breadcrumbs } from "../common/Breadcrumbs";
5
+
5
6
  const SidebarHeader = ({ title = "Dashboard", actions = null }) => {
6
7
  const { toggleSidebar, breadcrumbs } = useUiStore();
7
- return /* @__PURE__ */ jsxs("header", {
8
- className:
9
- "sticky top-0 z-30 h-16 bg-base-200 backdrop-blur border-b border-base-200 flex items-center justify-between px-4 lg:px-6",
10
- children: [
11
- /* @__PURE__ */ jsxs("div", {
12
- className: "flex items-center gap-3",
13
- children: [
14
- /* @__PURE__ */ jsx("button", {
15
- onClick: toggleSidebar,
16
- className:
17
- "btn btn-square border-none bg-base-200 shadow-none text-base-content lg:hidden",
18
- children: /* @__PURE__ */ jsx(FiMenu, { size: 22 }),
19
- }),
20
- breadcrumbs && breadcrumbs.length > 0
21
- ? /* @__PURE__ */ jsx(Breadcrumbs, { items: breadcrumbs })
22
- : /* @__PURE__ */ jsx("h1", {
23
- className: "text-xl font-bold",
24
- children: title,
25
- }),
26
- ],
27
- }),
28
- /* @__PURE__ */ jsx("div", {
29
- className: "flex items-center gap-2",
30
- children: actions,
31
- }),
32
- ],
33
- });
8
+
9
+ return (
10
+ <header className="sticky top-0 z-30 h-16 bg-base-200 backdrop-blur border-b border-base-200 flex items-center justify-between px-4 lg:px-6">
11
+ <div className="flex items-center gap-3">
12
+ <button
13
+ onClick={toggleSidebar}
14
+ className="btn btn-square border-none bg-base-200 shadow-none text-base-content lg:hidden"
15
+ >
16
+ <FiMenu size={22} />
17
+ </button>
18
+
19
+ {breadcrumbs && breadcrumbs.length > 0 ? (
20
+ <Breadcrumbs items={breadcrumbs} />
21
+ ) : (
22
+ <h1 className="text-xl font-bold">{title}</h1>
23
+ )}
24
+ </div>
25
+
26
+ <div className="flex items-center gap-2">{actions}</div>
27
+ </header>
28
+ );
34
29
  };
35
- var SidebarHeader_default = SidebarHeader;
36
- export { SidebarHeader_default as default };
30
+
31
+ export default SidebarHeader;
@@ -1,11 +1,11 @@
1
1
  const appConfig = {
2
2
  app: {
3
- name: "React App",
3
+ name: "My App",
4
4
  logo: "./logo.png",
5
- shortName: "React",
5
+ shortName: "App",
6
6
  description: "A React application built with @imksh/create",
7
7
  version: "1.0.0",
8
- author: "Karan Sharma",
8
+ author: "",
9
9
  },
10
10
 
11
11
  api: {
@@ -2,8 +2,8 @@ import { useState, useEffect } from "react";
2
2
 
3
3
  const useWindowSize = () => {
4
4
  const [size, setSize] = useState({
5
- width: window.innerWidth,
6
- height: window.innerHeight,
5
+ width: typeof window !== "undefined" ? window.innerWidth : 1024,
6
+ height: typeof window !== "undefined" ? window.innerHeight : 768,
7
7
  });
8
8
 
9
9
  useEffect(() => {
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
- import { Link } from "react-router-dom";
3
- import { ChevronRight } from "lucide-react";
2
+ import { Link, useNavigate } from "react-router-dom";
3
+ import { ArrowLeft, ChevronRight } from "lucide-react";
4
4
 
5
5
  export interface BreadcrumbItem {
6
6
  label: React.ReactNode;
@@ -14,23 +14,44 @@ interface BreadcrumbsProps {
14
14
  }
15
15
 
16
16
  export const Breadcrumbs = ({ items }: BreadcrumbsProps) => {
17
+ const navigation = useNavigate();
17
18
  return (
18
19
  <div className="flex items-center gap-1.5 text-sm font-bold text-base-content/50 flex-wrap">
20
+ <button
21
+ className="btn btn-primary btn-soft btn-square btn-sm mr-1 hidden md:flex"
22
+ onClick={() => navigation(-1)}
23
+ >
24
+ <ArrowLeft size={18} />
25
+ </button>
19
26
  {items.map((item, idx) => {
20
27
  const isLast = idx === items.length - 1;
21
28
  return (
22
29
  <React.Fragment key={idx}>
23
30
  {item.path && !isLast ? (
24
- <Link to={item.path} onClick={item.onClick} className="hover:text-primary transition-colors hidden sm:flex items-center gap-1">
31
+ <Link
32
+ to={item.path}
33
+ onClick={item.onClick}
34
+ className="hover:text-primary transition-colors hidden sm:flex items-center gap-1"
35
+ >
25
36
  {item.label}
26
37
  </Link>
27
38
  ) : item.onClick && !isLast ? (
28
- <button type="button" onClick={item.onClick} className="hover:text-primary transition-colors hidden sm:flex items-center gap-1">
39
+ <button
40
+ type="button"
41
+ onClick={item.onClick}
42
+ className="hover:text-primary transition-colors hidden sm:flex items-center gap-1"
43
+ >
29
44
  {item.label}
30
45
  </button>
31
46
  ) : (
32
47
  <div className="flex flex-col">
33
- <span className={isLast ? "text-lg md:text-xl font-bold leading-tight text-base-content tracking-tight truncate max-w-[200px] md:max-w-none" : ""}>
48
+ <span
49
+ className={
50
+ isLast
51
+ ? "text-lg md:text-xl font-bold leading-tight text-base-content tracking-tight truncate max-w-[200px] md:max-w-none"
52
+ : ""
53
+ }
54
+ >
34
55
  {item.label}
35
56
  </span>
36
57
  {item.subtitle && (
@@ -40,7 +61,12 @@ export const Breadcrumbs = ({ items }: BreadcrumbsProps) => {
40
61
  )}
41
62
  </div>
42
63
  )}
43
- {!isLast && <ChevronRight size={14} className="text-base-content/30 shrink-0 hidden sm:block" />}
64
+ {!isLast && (
65
+ <ChevronRight
66
+ size={14}
67
+ className="text-base-content/30 shrink-0 hidden sm:block"
68
+ />
69
+ )}
44
70
  </React.Fragment>
45
71
  );
46
72
  })}
@@ -14,78 +14,172 @@ export const Pagination: React.FC<PaginationProps> = ({
14
14
  }) => {
15
15
  if (totalPages <= 1) return null;
16
16
 
17
- const maxVisiblePages = 5;
18
- let startPage = Math.max(1, currentPage - Math.floor(maxVisiblePages / 2));
19
- const endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);
20
-
21
- if (endPage - startPage + 1 < maxVisiblePages) {
22
- startPage = Math.max(1, endPage - maxVisiblePages + 1);
17
+ // Desktop pagination: 5 pages with outer bounds & ellipsis
18
+ const maxDesktopPages = 5;
19
+ let startDesktopPage = Math.max(
20
+ 1,
21
+ currentPage - Math.floor(maxDesktopPages / 2),
22
+ );
23
+ let endDesktopPage = Math.min(
24
+ totalPages,
25
+ startDesktopPage + maxDesktopPages - 1,
26
+ );
27
+ if (endDesktopPage - startDesktopPage + 1 < maxDesktopPages) {
28
+ startDesktopPage = Math.max(1, endDesktopPage - maxDesktopPages + 1);
29
+ }
30
+ const desktopPages: number[] = [];
31
+ for (let i = startDesktopPage; i <= endDesktopPage; i++) {
32
+ desktopPages.push(i);
23
33
  }
24
34
 
25
- const pages = [];
26
- for (let i = startPage; i <= endPage; i++) {
27
- pages.push(i);
35
+ // Small screen pagination: exactly 3 page circles
36
+ const maxMobilePages = 3;
37
+ let startMobilePage = Math.max(
38
+ 1,
39
+ currentPage - Math.floor(maxMobilePages / 2),
40
+ );
41
+ let endMobilePage = Math.min(
42
+ totalPages,
43
+ startMobilePage + maxMobilePages - 1,
44
+ );
45
+ if (endMobilePage - startMobilePage + 1 < maxMobilePages) {
46
+ startMobilePage = Math.max(1, endMobilePage - maxMobilePages + 1);
47
+ }
48
+ const mobilePages: number[] = [];
49
+ for (let i = startMobilePage; i <= endMobilePage; i++) {
50
+ mobilePages.push(i);
28
51
  }
29
52
 
53
+ const baseBtnClass =
54
+ "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";
55
+ const activeBtnClass =
56
+ "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";
57
+ const disabledBtnClass =
58
+ "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";
59
+ const ellipsisClass =
60
+ "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";
61
+
30
62
  return (
31
- <div className="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">
32
- <button
33
- onClick={() => onPageChange(currentPage - 1)}
34
- disabled={currentPage === 1}
35
- className="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"
36
- aria-label="Previous Page"
37
- >
38
- <ChevronLeft className="w-5 h-5" />
39
- </button>
40
-
41
- {startPage > 1 && (
42
- <>
63
+ <>
64
+ {/* Mobile / Small Screen: 3 page circles + ellipses + boundary pages */}
65
+ <div className="flex sm:hidden items-center justify-center gap-1">
66
+ <button
67
+ onClick={() => onPageChange(currentPage - 1)}
68
+ disabled={currentPage === 1}
69
+ className={currentPage === 1 ? disabledBtnClass : baseBtnClass}
70
+ aria-label="Previous Page"
71
+ >
72
+ <ChevronLeft className="w-4 h-4" />
73
+ </button>
74
+
75
+ {startMobilePage > 1 && (
76
+ <>
77
+ <button
78
+ onClick={() => onPageChange(1)}
79
+ className={baseBtnClass}
80
+ >
81
+ 1
82
+ </button>
83
+ {startMobilePage > 2 && (
84
+ <span className={ellipsisClass}>...</span>
85
+ )}
86
+ </>
87
+ )}
88
+
89
+ {mobilePages.map((page) => (
43
90
  <button
44
- onClick={() => onPageChange(1)}
45
- className="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"
91
+ key={`mobile-${page}`}
92
+ onClick={() => onPageChange(page)}
93
+ className={currentPage === page ? activeBtnClass : baseBtnClass}
46
94
  >
47
- 1
95
+ {page}
48
96
  </button>
49
- {startPage > 2 && <span className="px-1 text-base-content/40">...</span>}
50
- </>
51
- )}
97
+ ))}
52
98
 
53
- {pages.map((page) => (
99
+ {endMobilePage < totalPages && (
100
+ <>
101
+ {endMobilePage < totalPages - 1 && (
102
+ <span className={ellipsisClass}>...</span>
103
+ )}
104
+ <button
105
+ onClick={() => onPageChange(totalPages)}
106
+ className={baseBtnClass}
107
+ >
108
+ {totalPages}
109
+ </button>
110
+ </>
111
+ )}
112
+
113
+ <button
114
+ onClick={() => onPageChange(currentPage + 1)}
115
+ disabled={currentPage === totalPages}
116
+ className={currentPage === totalPages ? disabledBtnClass : baseBtnClass}
117
+ aria-label="Next Page"
118
+ >
119
+ <ChevronRight className="w-4 h-4" />
120
+ </button>
121
+ </div>
122
+
123
+ {/* Desktop Screen: 5 page circles + ellipses + boundary pages */}
124
+ <div className="hidden sm:flex items-center justify-center gap-1.5">
54
125
  <button
55
- key={page}
56
- onClick={() => onPageChange(page)}
57
- className={`flex items-center justify-center w-10 h-10 rounded-xl transition-all duration-300 font-semibold ${
58
- currentPage === page
59
- ? "bg-primary text-primary-content shadow-lg shadow-primary/30 scale-105"
60
- : "text-base-content/70 hover:bg-base-content/10 hover:text-base-content"
61
- }`}
126
+ onClick={() => onPageChange(currentPage - 1)}
127
+ disabled={currentPage === 1}
128
+ className={currentPage === 1 ? disabledBtnClass : baseBtnClass}
129
+ aria-label="Previous Page"
62
130
  >
63
- {page}
131
+ <ChevronLeft className="w-4 h-4" />
64
132
  </button>
65
- ))}
66
133
 
67
- {endPage < totalPages && (
68
- <>
69
- {endPage < totalPages - 1 && (
70
- <span className="px-1 text-base-content/40">...</span>
71
- )}
134
+ {startDesktopPage > 1 && (
135
+ <>
136
+ <button
137
+ onClick={() => onPageChange(1)}
138
+ className={baseBtnClass}
139
+ >
140
+ 1
141
+ </button>
142
+ {startDesktopPage > 2 && (
143
+ <span className={ellipsisClass}>...</span>
144
+ )}
145
+ </>
146
+ )}
147
+
148
+ {desktopPages.map((page) => (
72
149
  <button
73
- onClick={() => onPageChange(totalPages)}
74
- className="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"
150
+ key={`desktop-${page}`}
151
+ onClick={() => onPageChange(page)}
152
+ className={currentPage === page ? activeBtnClass : baseBtnClass}
75
153
  >
76
- {totalPages}
154
+ {page}
77
155
  </button>
78
- </>
79
- )}
80
-
81
- <button
82
- onClick={() => onPageChange(currentPage + 1)}
83
- disabled={currentPage === totalPages}
84
- className="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"
85
- aria-label="Next Page"
86
- >
87
- <ChevronRight className="w-5 h-5" />
88
- </button>
89
- </div>
156
+ ))}
157
+
158
+ {endDesktopPage < totalPages && (
159
+ <>
160
+ {endDesktopPage < totalPages - 1 && (
161
+ <span className={ellipsisClass}>...</span>
162
+ )}
163
+ <button
164
+ onClick={() => onPageChange(totalPages)}
165
+ className={baseBtnClass}
166
+ >
167
+ {totalPages}
168
+ </button>
169
+ </>
170
+ )}
171
+
172
+ <button
173
+ onClick={() => onPageChange(currentPage + 1)}
174
+ disabled={currentPage === totalPages}
175
+ className={currentPage === totalPages ? disabledBtnClass : baseBtnClass}
176
+ aria-label="Next Page"
177
+ >
178
+ <ChevronRight className="w-4 h-4" />
179
+ </button>
180
+ </div>
181
+ </>
90
182
  );
91
183
  };
184
+
185
+ export default Pagination;