@lastbrain/module-auth 0.1.1 → 0.1.3
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/README.md +504 -0
- package/dist/api/admin/users.d.ts +36 -0
- package/dist/api/admin/users.d.ts.map +1 -0
- package/dist/api/admin/users.js +90 -0
- package/dist/api/auth/me.d.ts +17 -0
- package/dist/api/auth/me.d.ts.map +1 -0
- package/dist/api/auth/me.js +34 -0
- package/dist/api/auth/profile.d.ts +32 -0
- package/dist/api/auth/profile.d.ts.map +1 -0
- package/dist/api/auth/profile.js +108 -0
- package/dist/api/storage.d.ts +13 -0
- package/dist/api/storage.d.ts.map +1 -0
- package/dist/api/storage.js +47 -0
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +21 -0
- package/dist/web/admin/users.d.ts.map +1 -1
- package/dist/web/admin/users.js +87 -2
- package/dist/web/auth/dashboard.d.ts +1 -1
- package/dist/web/auth/dashboard.d.ts.map +1 -1
- package/dist/web/auth/dashboard.js +42 -2
- package/dist/web/auth/profile.d.ts.map +1 -1
- package/dist/web/auth/profile.js +152 -2
- package/dist/web/auth/reglage.d.ts.map +1 -1
- package/dist/web/auth/reglage.js +98 -2
- package/package.json +5 -4
- package/src/api/admin/users.ts +124 -0
- package/src/api/auth/me.ts +48 -0
- package/src/api/auth/profile.ts +156 -0
- package/src/api/public/signin.ts +31 -0
- package/src/api/storage.ts +63 -0
- package/src/auth.build.config.ts +137 -0
- package/src/components/Doc.tsx +310 -0
- package/src/index.ts +12 -0
- package/src/server.ts +2 -0
- package/src/web/admin/users.tsx +266 -0
- package/src/web/auth/dashboard.tsx +202 -0
- package/src/web/auth/profile.tsx +381 -0
- package/src/web/auth/reglage.tsx +304 -0
- package/src/web/public/ResetPassword.tsx +3 -0
- package/src/web/public/SignInPage.tsx +255 -0
- package/src/web/public/SignUpPage.tsx +293 -0
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import {
|
|
3
|
+
Card,
|
|
4
|
+
CardBody,
|
|
5
|
+
CardHeader,
|
|
6
|
+
Snippet,
|
|
7
|
+
Chip,
|
|
8
|
+
TableStructure,
|
|
9
|
+
} from "@lastbrain/ui";
|
|
10
|
+
|
|
11
|
+
export function AuthModuleDoc() {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-6">
|
|
14
|
+
{/* Header */}
|
|
15
|
+
<div className="flex items-start justify-between">
|
|
16
|
+
<div>
|
|
17
|
+
<h2 className="text-3xl font-bold mb-2">Module Auth</h2>
|
|
18
|
+
<p className="text-slate-600 dark:text-slate-400">
|
|
19
|
+
Authentification complète avec gestion des profils utilisateur
|
|
20
|
+
</p>
|
|
21
|
+
</div>
|
|
22
|
+
<Chip color="primary" variant="flat">
|
|
23
|
+
v0.1.0
|
|
24
|
+
</Chip>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Author */}
|
|
28
|
+
<Card>
|
|
29
|
+
<CardHeader>
|
|
30
|
+
<h3 className="text-xl font-semibold">📝 Informations</h3>
|
|
31
|
+
</CardHeader>
|
|
32
|
+
<CardBody className="space-y-2">
|
|
33
|
+
<div>
|
|
34
|
+
<span className="font-semibold">Auteur:</span> LastBrain Team
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<span className="font-semibold">Package:</span>{" "}
|
|
38
|
+
@lastbrain/module-auth
|
|
39
|
+
</div>
|
|
40
|
+
<div>
|
|
41
|
+
<span className="font-semibold">Dernière mise à jour:</span> 21
|
|
42
|
+
novembre 2025
|
|
43
|
+
</div>
|
|
44
|
+
</CardBody>
|
|
45
|
+
</Card>
|
|
46
|
+
|
|
47
|
+
{/* Pages disponibles */}
|
|
48
|
+
<Card>
|
|
49
|
+
<CardHeader>
|
|
50
|
+
<h3 className="text-xl font-semibold">📄 Pages Disponibles</h3>
|
|
51
|
+
</CardHeader>
|
|
52
|
+
<CardBody className="space-y-3">
|
|
53
|
+
<div>
|
|
54
|
+
<h4 className="font-semibold mb-2">Pages Publiques</h4>
|
|
55
|
+
<div className="space-y-2">
|
|
56
|
+
<div className="flex items-center gap-2">
|
|
57
|
+
<Chip size="sm" color="success" variant="flat">
|
|
58
|
+
GET
|
|
59
|
+
</Chip>
|
|
60
|
+
<code className="text-sm">/signin</code>
|
|
61
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
62
|
+
- Connexion
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div className="flex items-center gap-2">
|
|
66
|
+
<Chip size="sm" color="success" variant="flat">
|
|
67
|
+
GET
|
|
68
|
+
</Chip>
|
|
69
|
+
<code className="text-sm">/signup</code>
|
|
70
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
71
|
+
- Inscription
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<Chip size="sm" color="success" variant="flat">
|
|
76
|
+
GET
|
|
77
|
+
</Chip>
|
|
78
|
+
<code className="text-sm">/reset-password</code>
|
|
79
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
80
|
+
- Réinitialisation mot de passe
|
|
81
|
+
</span>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
<div>
|
|
86
|
+
<h4 className="font-semibold mb-2">Pages Protégées (Auth)</h4>
|
|
87
|
+
<div className="space-y-2">
|
|
88
|
+
<div className="flex items-center gap-2">
|
|
89
|
+
<Chip size="sm" color="primary" variant="flat">
|
|
90
|
+
GET
|
|
91
|
+
</Chip>
|
|
92
|
+
<code className="text-sm">/auth/dashboard</code>
|
|
93
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
94
|
+
- Tableau de bord
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
<Chip size="sm" color="primary" variant="flat">
|
|
99
|
+
GET
|
|
100
|
+
</Chip>
|
|
101
|
+
<code className="text-sm">/auth/profile</code>
|
|
102
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
103
|
+
- Profil utilisateur
|
|
104
|
+
</span>
|
|
105
|
+
</div>
|
|
106
|
+
<div className="flex items-center gap-2">
|
|
107
|
+
<Chip size="sm" color="primary" variant="flat">
|
|
108
|
+
GET
|
|
109
|
+
</Chip>
|
|
110
|
+
<code className="text-sm">/auth/reglage</code>
|
|
111
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
112
|
+
- Paramètres
|
|
113
|
+
</span>
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
<div>
|
|
118
|
+
<h4 className="font-semibold mb-2">Pages Admin</h4>
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<Chip size="sm" color="danger" variant="flat">
|
|
122
|
+
GET
|
|
123
|
+
</Chip>
|
|
124
|
+
<code className="text-sm">/admin/users</code>
|
|
125
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
126
|
+
- Gestion des utilisateurs
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</CardBody>
|
|
132
|
+
</Card>
|
|
133
|
+
|
|
134
|
+
{/* Routes API */}
|
|
135
|
+
<Card>
|
|
136
|
+
<CardHeader>
|
|
137
|
+
<h3 className="text-xl font-semibold">🔌 Routes API</h3>
|
|
138
|
+
</CardHeader>
|
|
139
|
+
<CardBody className="space-y-2">
|
|
140
|
+
<div className="flex items-center gap-2">
|
|
141
|
+
<Chip size="sm" color="warning" variant="flat">
|
|
142
|
+
POST
|
|
143
|
+
</Chip>
|
|
144
|
+
<code className="text-sm">/api/auth/signin</code>
|
|
145
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
146
|
+
- Authentification
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
<div className="flex items-center gap-2">
|
|
150
|
+
<Chip size="sm" color="warning" variant="flat">
|
|
151
|
+
POST
|
|
152
|
+
</Chip>
|
|
153
|
+
<code className="text-sm">/api/auth/signup</code>
|
|
154
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
155
|
+
- Création de compte
|
|
156
|
+
</span>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="flex items-center gap-2">
|
|
159
|
+
<Chip size="sm" color="warning" variant="flat">
|
|
160
|
+
POST
|
|
161
|
+
</Chip>
|
|
162
|
+
<code className="text-sm">/api/auth/signout</code>
|
|
163
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
164
|
+
- Déconnexion
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
<div className="flex items-center gap-2">
|
|
168
|
+
<Chip size="sm" color="warning" variant="flat">
|
|
169
|
+
POST
|
|
170
|
+
</Chip>
|
|
171
|
+
<code className="text-sm">/api/auth/reset-password</code>
|
|
172
|
+
<span className="text-slate-600 dark:text-slate-400">
|
|
173
|
+
- Réinitialisation
|
|
174
|
+
</span>
|
|
175
|
+
</div>
|
|
176
|
+
</CardBody>
|
|
177
|
+
</Card>
|
|
178
|
+
|
|
179
|
+
{/* Structure */}
|
|
180
|
+
<Card>
|
|
181
|
+
<CardHeader>
|
|
182
|
+
<h3 className="text-xl font-semibold">📁 Structure</h3>
|
|
183
|
+
</CardHeader>
|
|
184
|
+
<CardBody>
|
|
185
|
+
<Snippet
|
|
186
|
+
symbol=""
|
|
187
|
+
color="default"
|
|
188
|
+
size="sm"
|
|
189
|
+
hideSymbol
|
|
190
|
+
hideCopyButton
|
|
191
|
+
>
|
|
192
|
+
<div className="flex flex-col gap-1 text-xs font-mono">
|
|
193
|
+
<span>module-auth/</span>
|
|
194
|
+
<span>├── src/</span>
|
|
195
|
+
<span>│ ├── web/</span>
|
|
196
|
+
<span>│ │ ├── signin/</span>
|
|
197
|
+
<span>│ │ ├── signup/</span>
|
|
198
|
+
<span>│ │ ├── dashboard/</span>
|
|
199
|
+
<span>│ │ ├── profile/</span>
|
|
200
|
+
<span>│ │ └── reglage/</span>
|
|
201
|
+
<span>│ ├── api/</span>
|
|
202
|
+
<span>│ │ ├── signin.ts</span>
|
|
203
|
+
<span>│ │ ├── signup.ts</span>
|
|
204
|
+
<span>│ │ ├── signout.ts</span>
|
|
205
|
+
<span>│ │ └── reset-password.ts</span>
|
|
206
|
+
<span>│ ├── components/</span>
|
|
207
|
+
<span>│ │ └── Doc.tsx</span>
|
|
208
|
+
<span>│ └── auth.build.config.ts</span>
|
|
209
|
+
<span>└── supabase/</span>
|
|
210
|
+
<span> └── migrations/</span>
|
|
211
|
+
<span> └── 20251112000000_user_init.sql</span>
|
|
212
|
+
</div>
|
|
213
|
+
</Snippet>
|
|
214
|
+
</CardBody>
|
|
215
|
+
</Card>
|
|
216
|
+
|
|
217
|
+
{/* Tables de données */}
|
|
218
|
+
<Card>
|
|
219
|
+
<CardHeader>
|
|
220
|
+
<h3 className="text-xl font-semibold">🗄️ Tables de Données</h3>
|
|
221
|
+
</CardHeader>
|
|
222
|
+
<CardBody className="space-y-4">
|
|
223
|
+
<TableStructure
|
|
224
|
+
tableName="user_profil"
|
|
225
|
+
title="user_profil"
|
|
226
|
+
description="Profil étendu des utilisateurs (prénom, nom, téléphone, avatar, bio, etc.)"
|
|
227
|
+
/>
|
|
228
|
+
<TableStructure
|
|
229
|
+
tableName="user_address"
|
|
230
|
+
title="user_address"
|
|
231
|
+
description="Adresses des utilisateurs (livraison, facturation)"
|
|
232
|
+
/>
|
|
233
|
+
<TableStructure
|
|
234
|
+
tableName="user_notifications"
|
|
235
|
+
title="user_notifications"
|
|
236
|
+
description="Préférences de notifications (email, SMS, push)"
|
|
237
|
+
/>
|
|
238
|
+
</CardBody>
|
|
239
|
+
</Card>
|
|
240
|
+
|
|
241
|
+
{/* Installation */}
|
|
242
|
+
<Card>
|
|
243
|
+
<CardHeader>
|
|
244
|
+
<h3 className="text-xl font-semibold">📦 Installation</h3>
|
|
245
|
+
</CardHeader>
|
|
246
|
+
<CardBody className="space-y-4">
|
|
247
|
+
<div className="flex flex-col gap-2">
|
|
248
|
+
<Snippet symbol="" color="default" size="sm">
|
|
249
|
+
pnpm lastbrain add-module auth
|
|
250
|
+
</Snippet>
|
|
251
|
+
<Snippet symbol="" color="default" size="sm">
|
|
252
|
+
pnpm build:modules
|
|
253
|
+
</Snippet>
|
|
254
|
+
<Snippet symbol="" color="default" size="sm">
|
|
255
|
+
supabase migration up
|
|
256
|
+
</Snippet>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Danger Zone */}
|
|
260
|
+
<div className="border-2 border-danger rounded-lg p-4 mt-6">
|
|
261
|
+
<h4 className="text-lg font-semibold text-danger mb-3">
|
|
262
|
+
⚠️ Danger Zone
|
|
263
|
+
</h4>
|
|
264
|
+
<p className="text-sm text-slate-600 dark:text-slate-400 mb-3">
|
|
265
|
+
La suppression du module supprimera toutes les pages, routes API
|
|
266
|
+
et migrations associées. Cette action est irréversible.
|
|
267
|
+
</p>
|
|
268
|
+
<div className="flex flex-col gap-2">
|
|
269
|
+
<Snippet symbol="" color="danger" size="sm">
|
|
270
|
+
pnpm lastbrain remove-module auth
|
|
271
|
+
</Snippet>
|
|
272
|
+
<Snippet symbol="" color="danger" size="sm">
|
|
273
|
+
pnpm build:modules
|
|
274
|
+
</Snippet>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
</CardBody>
|
|
278
|
+
</Card>
|
|
279
|
+
|
|
280
|
+
{/* Utilisation */}
|
|
281
|
+
<Card>
|
|
282
|
+
<CardHeader>
|
|
283
|
+
<h3 className="text-xl font-semibold">💡 Utilisation</h3>
|
|
284
|
+
</CardHeader>
|
|
285
|
+
<CardBody className="space-y-3">
|
|
286
|
+
<div>
|
|
287
|
+
<h4 className="font-semibold mb-2">Hooks disponibles</h4>
|
|
288
|
+
<Snippet symbol="" color="default" size="sm">
|
|
289
|
+
import {"{ useAuthSession }"} from "@lastbrain/module-auth";
|
|
290
|
+
</Snippet>
|
|
291
|
+
</div>
|
|
292
|
+
<div>
|
|
293
|
+
<h4 className="font-semibold mb-2 mt-4">Exemple d'utilisation</h4>
|
|
294
|
+
<Snippet symbol="" color="default" size="sm" hideSymbol>
|
|
295
|
+
<div className="flex flex-col gap-1">
|
|
296
|
+
<span>const {"{ session, user }"} = useAuthSession();</span>
|
|
297
|
+
<span>
|
|
298
|
+
if (!session) return <p>Non connecté</p>;
|
|
299
|
+
</span>
|
|
300
|
+
<span>
|
|
301
|
+
return <div>Bonjour {"{user.email}"}</div>;
|
|
302
|
+
</span>
|
|
303
|
+
</div>
|
|
304
|
+
</Snippet>
|
|
305
|
+
</div>
|
|
306
|
+
</CardBody>
|
|
307
|
+
</Card>
|
|
308
|
+
</div>
|
|
309
|
+
);
|
|
310
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Client Components uniquement
|
|
2
|
+
export { SignInPage } from "./web/public/SignInPage.js";
|
|
3
|
+
export { SignUpPage } from "./web/public/SignUpPage.js";
|
|
4
|
+
export { ResetPassword } from "./web/public/ResetPassword.js";
|
|
5
|
+
export { DashboardPage } from "./web/auth/dashboard.js";
|
|
6
|
+
export { ProfilePage } from "./web/auth/profile.js";
|
|
7
|
+
export { ReglagePage } from "./web/auth/reglage.js";
|
|
8
|
+
export { AdminUsersPage } from "./web/admin/users.js";
|
|
9
|
+
// Documentation
|
|
10
|
+
export { AuthModuleDoc } from "./components/Doc.js";
|
|
11
|
+
// Configuration de build (utilisée par les scripts)
|
|
12
|
+
export { default as authBuildConfig } from "./auth.build.config.js";
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
CardHeader,
|
|
8
|
+
Table,
|
|
9
|
+
TableHeader,
|
|
10
|
+
TableColumn,
|
|
11
|
+
TableBody,
|
|
12
|
+
TableRow,
|
|
13
|
+
TableCell,
|
|
14
|
+
Spinner,
|
|
15
|
+
Chip,
|
|
16
|
+
Input,
|
|
17
|
+
Button,
|
|
18
|
+
Pagination,
|
|
19
|
+
Avatar,
|
|
20
|
+
addToast,
|
|
21
|
+
} from "@lastbrain/ui";
|
|
22
|
+
import { Users, Search, RefreshCw } from "lucide-react";
|
|
23
|
+
|
|
24
|
+
interface User {
|
|
25
|
+
id: string;
|
|
26
|
+
email: string;
|
|
27
|
+
created_at: string;
|
|
28
|
+
profile: {
|
|
29
|
+
first_name?: string;
|
|
30
|
+
last_name?: string;
|
|
31
|
+
avatar_url?: string;
|
|
32
|
+
company?: string;
|
|
33
|
+
location?: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface PaginationData {
|
|
38
|
+
page: number;
|
|
39
|
+
per_page: number;
|
|
40
|
+
total: number;
|
|
41
|
+
total_pages: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function AdminUsersPage() {
|
|
45
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
46
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
49
|
+
const [pagination, setPagination] = useState<PaginationData>({
|
|
50
|
+
page: 1,
|
|
51
|
+
per_page: 20,
|
|
52
|
+
total: 0,
|
|
53
|
+
total_pages: 0,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
fetchUsers();
|
|
58
|
+
}, [pagination.page]);
|
|
59
|
+
|
|
60
|
+
const fetchUsers = async () => {
|
|
61
|
+
try {
|
|
62
|
+
setIsLoading(true);
|
|
63
|
+
const params = new URLSearchParams({
|
|
64
|
+
page: pagination.page.toString(),
|
|
65
|
+
per_page: pagination.per_page.toString(),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
if (searchQuery) {
|
|
69
|
+
params.append("search", searchQuery);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const response = await fetch(`/api/admin/users?${params}`);
|
|
73
|
+
|
|
74
|
+
if (response.status === 403) {
|
|
75
|
+
setError("You don't have permission to access this page");
|
|
76
|
+
addToast({
|
|
77
|
+
title: "Access Denied",
|
|
78
|
+
description: "Superadmin access required",
|
|
79
|
+
color: "danger",
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new Error("Failed to fetch users");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = await response.json();
|
|
89
|
+
setUsers(result.data || []);
|
|
90
|
+
if (result.pagination) {
|
|
91
|
+
setPagination(result.pagination);
|
|
92
|
+
}
|
|
93
|
+
setError(null);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
setError(err instanceof Error ? err.message : "An error occurred");
|
|
96
|
+
addToast({
|
|
97
|
+
title: "Error",
|
|
98
|
+
description: "Failed to load users",
|
|
99
|
+
color: "danger",
|
|
100
|
+
});
|
|
101
|
+
} finally {
|
|
102
|
+
setIsLoading(false);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleSearch = () => {
|
|
107
|
+
setPagination((prev) => ({ ...prev, page: 1 }));
|
|
108
|
+
fetchUsers();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const handlePageChange = (page: number) => {
|
|
112
|
+
setPagination((prev) => ({ ...prev, page }));
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const formatDate = (dateString: string) => {
|
|
116
|
+
return new Date(dateString).toLocaleDateString("en-US", {
|
|
117
|
+
year: "numeric",
|
|
118
|
+
month: "short",
|
|
119
|
+
day: "numeric",
|
|
120
|
+
});
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if (error && users.length === 0) {
|
|
124
|
+
return (
|
|
125
|
+
<div className="pt-12 pb-12 max-w-7xl mx-auto px-4">
|
|
126
|
+
<Card>
|
|
127
|
+
<CardBody>
|
|
128
|
+
<p className="text-danger">{error}</p>
|
|
129
|
+
</CardBody>
|
|
130
|
+
</Card>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<div className="pt-12 pb-12 max-w-7xl mx-auto px-4">
|
|
137
|
+
<div className="flex items-center gap-2 mb-8">
|
|
138
|
+
<Users className="w-8 h-8" />
|
|
139
|
+
<h1 className="text-3xl font-bold">User Management</h1>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<Card>
|
|
143
|
+
<CardHeader>
|
|
144
|
+
<div className="flex flex-col md:flex-row gap-4 w-full">
|
|
145
|
+
<div className="flex gap-2 flex-1">
|
|
146
|
+
<Input
|
|
147
|
+
placeholder="Search by email or name..."
|
|
148
|
+
value={searchQuery}
|
|
149
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
150
|
+
onKeyPress={(e) => {
|
|
151
|
+
if (e.key === "Enter") {
|
|
152
|
+
handleSearch();
|
|
153
|
+
}
|
|
154
|
+
}}
|
|
155
|
+
startContent={<Search className="w-4 h-4 text-default-400" />}
|
|
156
|
+
className="flex-1"
|
|
157
|
+
/>
|
|
158
|
+
<Button
|
|
159
|
+
color="primary"
|
|
160
|
+
onPress={handleSearch}
|
|
161
|
+
isDisabled={isLoading}
|
|
162
|
+
>
|
|
163
|
+
Search
|
|
164
|
+
</Button>
|
|
165
|
+
</div>
|
|
166
|
+
<Button
|
|
167
|
+
variant="flat"
|
|
168
|
+
onPress={fetchUsers}
|
|
169
|
+
isDisabled={isLoading}
|
|
170
|
+
startContent={<RefreshCw className="w-4 h-4" />}
|
|
171
|
+
>
|
|
172
|
+
Refresh
|
|
173
|
+
</Button>
|
|
174
|
+
</div>
|
|
175
|
+
</CardHeader>
|
|
176
|
+
<CardBody>
|
|
177
|
+
{isLoading ? (
|
|
178
|
+
<div className="flex justify-center items-center py-12">
|
|
179
|
+
<Spinner size="lg" label="Loading users..." />
|
|
180
|
+
</div>
|
|
181
|
+
) : users.length === 0 ? (
|
|
182
|
+
<div className="text-center py-12 text-default-500">
|
|
183
|
+
No users found
|
|
184
|
+
</div>
|
|
185
|
+
) : (
|
|
186
|
+
<>
|
|
187
|
+
<Table aria-label="Users table">
|
|
188
|
+
<TableHeader>
|
|
189
|
+
<TableColumn>USER</TableColumn>
|
|
190
|
+
<TableColumn>EMAIL</TableColumn>
|
|
191
|
+
<TableColumn>COMPANY</TableColumn>
|
|
192
|
+
<TableColumn>LOCATION</TableColumn>
|
|
193
|
+
<TableColumn>CREATED</TableColumn>
|
|
194
|
+
<TableColumn>STATUS</TableColumn>
|
|
195
|
+
</TableHeader>
|
|
196
|
+
<TableBody>
|
|
197
|
+
{users.map((user) => {
|
|
198
|
+
const fullName =
|
|
199
|
+
user.profile?.first_name && user.profile?.last_name
|
|
200
|
+
? `${user.profile.first_name} ${user.profile.last_name}`
|
|
201
|
+
: "N/A";
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<TableRow key={user.id}>
|
|
205
|
+
<TableCell>
|
|
206
|
+
<div className="flex items-center gap-2">
|
|
207
|
+
<Avatar
|
|
208
|
+
src={user.profile?.avatar_url}
|
|
209
|
+
name={fullName}
|
|
210
|
+
size="sm"
|
|
211
|
+
/>
|
|
212
|
+
<span className="text-small font-medium">
|
|
213
|
+
{fullName}
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
</TableCell>
|
|
217
|
+
<TableCell>
|
|
218
|
+
<span className="text-small">{user.email}</span>
|
|
219
|
+
</TableCell>
|
|
220
|
+
<TableCell>
|
|
221
|
+
<span className="text-small">
|
|
222
|
+
{user.profile?.company || "-"}
|
|
223
|
+
</span>
|
|
224
|
+
</TableCell>
|
|
225
|
+
<TableCell>
|
|
226
|
+
<span className="text-small">
|
|
227
|
+
{user.profile?.location || "-"}
|
|
228
|
+
</span>
|
|
229
|
+
</TableCell>
|
|
230
|
+
<TableCell>
|
|
231
|
+
<span className="text-small">
|
|
232
|
+
{formatDate(user.created_at)}
|
|
233
|
+
</span>
|
|
234
|
+
</TableCell>
|
|
235
|
+
<TableCell>
|
|
236
|
+
<Chip color="success" size="sm" variant="flat">
|
|
237
|
+
Active
|
|
238
|
+
</Chip>
|
|
239
|
+
</TableCell>
|
|
240
|
+
</TableRow>
|
|
241
|
+
);
|
|
242
|
+
})}
|
|
243
|
+
</TableBody>
|
|
244
|
+
</Table>
|
|
245
|
+
|
|
246
|
+
{pagination.total_pages > 1 && (
|
|
247
|
+
<div className="flex justify-center mt-4">
|
|
248
|
+
<Pagination
|
|
249
|
+
total={pagination.total_pages}
|
|
250
|
+
page={pagination.page}
|
|
251
|
+
onChange={handlePageChange}
|
|
252
|
+
showControls
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
|
|
257
|
+
<div className="mt-4 text-small text-default-500 text-center">
|
|
258
|
+
Showing {users.length} of {pagination.total} users
|
|
259
|
+
</div>
|
|
260
|
+
</>
|
|
261
|
+
)}
|
|
262
|
+
</CardBody>
|
|
263
|
+
</Card>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|