@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.
- package/package.json +1 -1
- package/src/utils/askTemplate.js +2 -1
- package/templates/layers/electron/base/electron/main.js +262 -0
- package/templates/layers/electron/base/electron/preload.js +37 -0
- package/templates/layers/electron/base/electron-builder.json5 +68 -0
- package/templates/layers/electron/javascript/_gitignore +38 -0
- package/templates/layers/electron/javascript/package.json +49 -0
- package/templates/layers/electron/javascript/src/components/layout/SidebarLayout.jsx +43 -0
- package/templates/layers/electron/javascript/src/components/ui/TitleBar.jsx +130 -0
- package/templates/layers/electron/javascript/src/config/appConfig.js +51 -0
- package/templates/layers/electron/javascript/src/hooks/useElectron.js +68 -0
- package/templates/layers/electron/javascript/src/main.jsx +17 -0
- package/templates/layers/electron/javascript/src/store/useUiStore.js +59 -0
- package/templates/layers/electron/javascript/vite.config.js +29 -0
- package/templates/layers/electron/typescript/_gitignore +38 -0
- package/templates/layers/electron/typescript/electron/main.ts +213 -0
- package/templates/layers/electron/typescript/electron/preload.ts +52 -0
- package/templates/layers/electron/typescript/package.json +52 -0
- package/templates/layers/electron/typescript/src/components/layout/SidebarLayout.tsx +42 -0
- package/templates/layers/electron/typescript/src/components/ui/TitleBar.tsx +120 -0
- package/templates/layers/electron/typescript/src/config/appConfig.ts +52 -0
- package/templates/layers/electron/typescript/src/hooks/useElectron.ts +68 -0
- package/templates/layers/electron/typescript/src/main.tsx +17 -0
- package/templates/layers/electron/typescript/src/store/useUiStore.ts +97 -0
- package/templates/layers/electron/typescript/src/vite-env.d.ts +13 -0
- package/templates/layers/electron/typescript/tsconfig.json +25 -0
- package/templates/layers/electron/typescript/tsconfig.node.json +11 -0
- package/templates/layers/electron/typescript/vite.config.ts +29 -0
- package/templates/layers/next/javascript/src/components/common/Breadcrumbs.jsx +44 -6
- package/templates/layers/next/javascript/src/components/common/Loading.jsx +1 -0
- package/templates/layers/next/javascript/src/components/common/LottieIcon.jsx +1 -0
- package/templates/layers/next/javascript/src/components/common/Pagination.jsx +170 -0
- package/templates/layers/next/javascript/src/components/common/SidebarModal.jsx +180 -0
- package/templates/layers/next/javascript/src/components/common/StatsCard.jsx +168 -0
- package/templates/layers/next/javascript/src/components/common/Table.jsx +191 -41
- package/templates/layers/next/javascript/src/components/common/Tabs.jsx +187 -0
- package/templates/layers/next/javascript/src/components/common/ToggleSwitch.jsx +4 -3
- package/templates/layers/next/javascript/src/components/layout/ProtectedRoute.jsx +2 -1
- package/templates/layers/next/javascript/src/components/layout/PublicLayout.jsx +3 -6
- package/templates/layers/next/javascript/src/components/layout/SidebarLayout.jsx +4 -17
- package/templates/layers/next/javascript/src/components/modals/ConfirmModal.jsx +4 -12
- package/templates/layers/next/javascript/src/components/modals/Modal.jsx +50 -19
- package/templates/layers/next/javascript/src/components/ui/Input.jsx +50 -0
- package/templates/layers/next/{typescript/src/components/ui/sidebar.tsx → javascript/src/components/ui/Sidebar.jsx} +65 -53
- package/templates/layers/next/javascript/src/components/ui/SidebarHeader.jsx +11 -6
- package/templates/layers/next/javascript/src/config/appConfig.js +4 -4
- package/templates/layers/next/javascript/src/hooks/useWindowSize.js +2 -3
- package/templates/layers/next/javascript/src/store/useUiStore.js +9 -4
- package/templates/layers/next/typescript/src/components/common/Breadcrumbs.tsx +57 -8
- package/templates/layers/next/typescript/src/components/common/Loading.tsx +1 -0
- package/templates/layers/next/typescript/src/components/common/Pagination.tsx +186 -0
- package/templates/layers/next/typescript/src/components/common/SidebarModal.tsx +200 -0
- package/templates/layers/next/typescript/src/components/common/StatsCard.tsx +232 -0
- package/templates/layers/next/typescript/src/components/common/Table.tsx +201 -35
- package/templates/layers/next/typescript/src/components/common/Tabs.tsx +190 -0
- package/templates/layers/next/typescript/src/components/common/ToggleSwitch.tsx +12 -8
- package/templates/layers/next/typescript/src/components/layout/PublicLayout.tsx +8 -6
- package/templates/layers/next/typescript/src/components/layout/SidebarLayout.tsx +8 -17
- package/templates/layers/next/typescript/src/components/modals/ConfirmModal.tsx +1 -0
- package/templates/layers/next/typescript/src/components/modals/Modal.tsx +67 -21
- package/templates/layers/next/typescript/src/components/ui/Button.tsx +1 -0
- package/templates/layers/next/typescript/src/components/ui/Input.tsx +40 -60
- package/templates/layers/next/{javascript/src/components/ui/sidebar.jsx → typescript/src/components/ui/Sidebar.tsx} +77 -44
- package/templates/layers/next/typescript/src/components/ui/SidebarHeader.tsx +16 -10
- package/templates/layers/next/typescript/src/config/appConfig.ts +4 -4
- package/templates/layers/next/typescript/src/hooks/useWindowSize.ts +2 -3
- package/templates/layers/next/typescript/src/store/useUiStore.ts +14 -4
- package/templates/layers/node/javascript/package.json +2 -1
- package/templates/layers/node/javascript/src/controllers/push.controller.js +35 -14
- package/templates/layers/node/javascript/src/models/pushSubscription.model.js +5 -0
- package/templates/layers/node/javascript/src/utils/pushHelper.js +59 -12
- package/templates/layers/node/typescript/package-lock.json +3795 -0
- package/templates/layers/node/typescript/package.json +3 -2
- package/templates/layers/node/typescript/src/controllers/push.controller.ts +35 -14
- package/templates/layers/node/typescript/src/models/pushSubscription.model.ts +6 -0
- package/templates/layers/node/typescript/src/services/auth.service.ts +3 -3
- package/templates/layers/node/typescript/src/utils/pushHelper.ts +66 -12
- package/templates/layers/node-prisma/javascript/package.json +6 -3
- package/templates/layers/node-prisma/javascript/prisma/schema.prisma +20 -9
- package/templates/layers/node-prisma/javascript/prisma.config.js +12 -0
- package/templates/layers/node-prisma/javascript/src/config/{db.js → prisma.js} +9 -1
- package/templates/layers/node-prisma/javascript/src/controllers/push.controller.js +58 -19
- package/templates/layers/node-prisma/javascript/src/utils/pushHelper.js +62 -14
- package/templates/layers/node-prisma/typescript/package-lock.json +4808 -0
- package/templates/layers/node-prisma/typescript/package.json +8 -4
- package/templates/layers/node-prisma/typescript/prisma/schema.prisma +20 -9
- package/templates/layers/node-prisma/typescript/prisma.config.ts +12 -0
- package/templates/layers/node-prisma/typescript/src/config/{db.ts → prisma.ts} +9 -1
- package/templates/layers/node-prisma/typescript/src/controllers/push.controller.ts +67 -22
- package/templates/layers/node-prisma/typescript/src/index.ts +1 -1
- package/templates/layers/node-prisma/typescript/src/middleware/auth.middleware.ts +15 -5
- package/templates/layers/node-prisma/typescript/src/services/auth.service.ts +134 -128
- package/templates/layers/node-prisma/typescript/src/utils/pushHelper.ts +69 -14
- package/templates/layers/prisma/typescript/prisma/schema.prisma +35 -10
- package/templates/layers/react/javascript/src/components/common/Breadcrumbs.jsx +62 -58
- package/templates/layers/react/javascript/src/components/common/Pagination.jsx +164 -76
- package/templates/layers/react/javascript/src/components/common/SidebarModal.jsx +179 -0
- package/templates/layers/react/javascript/src/components/common/StatsCard.jsx +167 -0
- package/templates/layers/react/javascript/src/components/common/Table.jsx +248 -132
- package/templates/layers/react/javascript/src/components/common/Tabs.jsx +164 -112
- package/templates/layers/react/javascript/src/components/common/ToggleSwitch.jsx +3 -3
- package/templates/layers/react/javascript/src/components/layout/SidebarLayout.jsx +4 -12
- package/templates/layers/react/javascript/src/components/modals/Modal.jsx +53 -18
- package/templates/layers/react/javascript/src/components/ui/Input.jsx +49 -0
- package/templates/layers/react/javascript/src/components/ui/Sidebar.jsx +215 -0
- package/templates/layers/react/javascript/src/components/ui/SidebarHeader.jsx +25 -30
- package/templates/layers/react/javascript/src/config/appConfig.js +3 -3
- package/templates/layers/react/javascript/src/hooks/useWindowSize.js +2 -2
- package/templates/layers/react/typescript/src/components/common/Breadcrumbs.tsx +32 -6
- package/templates/layers/react/typescript/src/components/common/Pagination.tsx +151 -57
- package/templates/layers/react/typescript/src/components/common/SidebarModal.tsx +199 -0
- package/templates/layers/react/typescript/src/components/common/StatsCard.tsx +231 -0
- package/templates/layers/react/typescript/src/components/common/Table.tsx +197 -34
- package/templates/layers/react/typescript/src/components/common/Tabs.tsx +56 -12
- package/templates/layers/react/typescript/src/components/common/ToggleSwitch.tsx +15 -3
- package/templates/layers/react/typescript/src/components/layout/SidebarLayout.tsx +3 -6
- package/templates/layers/react/typescript/src/components/modals/Modal.tsx +151 -0
- package/templates/layers/react/typescript/src/components/ui/Input.tsx +58 -0
- package/templates/layers/react/typescript/src/components/ui/Sidebar.tsx +116 -53
- package/templates/layers/react/typescript/src/components/ui/SidebarHeader.tsx +5 -4
- package/templates/layers/react/typescript/src/config/appConfig.ts +3 -3
- package/templates/layers/react/typescript/src/hooks/useWindowSize.ts +2 -3
- package/templates/layers/react-native/base/src/components/ui/BiometricLock.tsx +2 -1
- package/templates/layers/react-native/base/src/config/appConfig.ts +44 -0
- package/templates/presets/electron/preset.json +9 -0
- package/templates/presets/electron-ts/preset.json +9 -0
- 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
|
|
12
|
-
email
|
|
13
|
-
name
|
|
14
|
-
phone
|
|
15
|
-
password
|
|
16
|
-
role
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
1
|
+
import React from "react";
|
|
2
2
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
export const Pagination = ({ currentPage, totalPages, onPageChange }) => {
|
|
4
5
|
if (totalPages <= 1) return null;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
12
|
-
for (let i =
|
|
13
|
-
|
|
20
|
+
const desktopPages = [];
|
|
21
|
+
for (let i = startDesktopPage; i <= endDesktopPage; i++) {
|
|
22
|
+
desktopPages.push(i);
|
|
14
23
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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;
|