@lastbrain/module-auth 0.1.21 → 0.1.23
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/dist/api/admin/signup-stats.d.ts +21 -0
- package/dist/api/admin/signup-stats.d.ts.map +1 -0
- package/dist/api/admin/signup-stats.js +75 -0
- package/dist/api/admin/users-by-source.d.ts +22 -0
- package/dist/api/admin/users-by-source.d.ts.map +1 -0
- package/dist/api/admin/users-by-source.js +56 -0
- package/dist/api/public/signup.d.ts +10 -0
- package/dist/api/public/signup.d.ts.map +1 -0
- package/dist/api/public/signup.js +71 -0
- package/dist/auth.build.config.d.ts.map +1 -1
- package/dist/auth.build.config.js +26 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/web/admin/signup-stats.d.ts +2 -0
- package/dist/web/admin/signup-stats.d.ts.map +1 -0
- package/dist/web/admin/signup-stats.js +50 -0
- package/dist/web/admin/user-detail.d.ts.map +1 -1
- package/dist/web/admin/user-detail.js +5 -1
- package/dist/web/admin/users-by-signup-source.d.ts +2 -0
- package/dist/web/admin/users-by-signup-source.d.ts.map +1 -0
- package/dist/web/admin/users-by-signup-source.js +79 -0
- package/dist/web/auth/folder.d.ts.map +1 -1
- package/dist/web/auth/folder.js +3 -5
- package/dist/web/public/SignUpPage.d.ts.map +1 -1
- package/dist/web/public/SignUpPage.js +15 -23
- package/package.json +6 -6
- package/src/api/admin/signup-stats.ts +109 -0
- package/src/api/admin/users/[id]/notifications.ts +5 -5
- package/src/api/admin/users/[id].ts +5 -5
- package/src/api/admin/users-by-source.ts +87 -0
- package/src/api/admin/users.ts +4 -4
- package/src/api/auth/me.ts +1 -1
- package/src/api/auth/profile.ts +4 -4
- package/src/api/public/signup.ts +106 -0
- package/src/api/storage.ts +3 -3
- package/src/auth.build.config.ts +27 -0
- package/src/index.ts +1 -0
- package/src/server.ts +1 -0
- package/src/web/admin/signup-stats.tsx +304 -0
- package/src/web/admin/user-detail.tsx +19 -3
- package/src/web/admin/users-by-signup-source.tsx +262 -0
- package/src/web/admin/users.tsx +1 -1
- package/src/web/auth/dashboard.tsx +1 -1
- package/src/web/auth/folder.tsx +4 -14
- package/src/web/auth/profile.tsx +3 -3
- package/src/web/public/SignUpPage.tsx +17 -26
- package/supabase/migrations/20251112000000_user_init.sql +18 -1
- package/supabase/migrations/20251112000001_auto_profile_and_admin_view.sql +10 -2
- package/supabase/migrations/20251124000001_add_get_admin_user_details.sql +2 -1
- package/supabase/migrations-down/20251204000000_add_signup_source.sql +12 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
CardHeader,
|
|
8
|
+
Chip,
|
|
9
|
+
Spinner,
|
|
10
|
+
Tab,
|
|
11
|
+
Table,
|
|
12
|
+
TableBody,
|
|
13
|
+
TableCell,
|
|
14
|
+
TableColumn,
|
|
15
|
+
TableHeader,
|
|
16
|
+
TableRow,
|
|
17
|
+
Tabs,
|
|
18
|
+
} from "@lastbrain/ui";
|
|
19
|
+
import { BarChart3, TrendingUp } from "lucide-react";
|
|
20
|
+
|
|
21
|
+
interface SignupStats {
|
|
22
|
+
total: number;
|
|
23
|
+
bySource: {
|
|
24
|
+
lastbrain: number;
|
|
25
|
+
recipe: number;
|
|
26
|
+
};
|
|
27
|
+
byDate: Array<{
|
|
28
|
+
date: string;
|
|
29
|
+
lastbrain: number;
|
|
30
|
+
recipe: number;
|
|
31
|
+
total: number;
|
|
32
|
+
}>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function SignupStatsPage() {
|
|
36
|
+
const [stats, setStats] = useState<SignupStats | null>(null);
|
|
37
|
+
const [loading, setLoading] = useState(true);
|
|
38
|
+
const [error, setError] = useState<string | null>(null);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
fetchStats();
|
|
42
|
+
}, []);
|
|
43
|
+
|
|
44
|
+
const fetchStats = async () => {
|
|
45
|
+
try {
|
|
46
|
+
setLoading(true);
|
|
47
|
+
const response = await fetch("/api/admin/signup-stats");
|
|
48
|
+
|
|
49
|
+
if (!response.ok) {
|
|
50
|
+
throw new Error("Erreur lors du chargement des statistiques");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = await response.json();
|
|
54
|
+
setStats(result.data);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
setError(
|
|
57
|
+
err instanceof Error ? err.message : "Erreur lors du chargement"
|
|
58
|
+
);
|
|
59
|
+
} finally {
|
|
60
|
+
setLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (loading) {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex justify-center items-center min-h-screen">
|
|
67
|
+
<Spinner size="lg" label="Chargement des statistiques..." />
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (error || !stats) {
|
|
73
|
+
return (
|
|
74
|
+
<div className="p-6">
|
|
75
|
+
<Card className="border border-danger-200 bg-danger-50/50">
|
|
76
|
+
<CardBody>
|
|
77
|
+
<p className="text-danger-600">{error || "Erreur de chargement"}</p>
|
|
78
|
+
</CardBody>
|
|
79
|
+
</Card>
|
|
80
|
+
</div>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const lastbrainPercentage = (
|
|
85
|
+
(stats.bySource.lastbrain / stats.total) *
|
|
86
|
+
100
|
|
87
|
+
).toFixed(1);
|
|
88
|
+
const recipePercentage = (
|
|
89
|
+
(stats.bySource.recipe / stats.total) *
|
|
90
|
+
100
|
|
91
|
+
).toFixed(1);
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="space-y-6 px-2 md:p-6">
|
|
95
|
+
{/* Header */}
|
|
96
|
+
<div className="flex items-center gap-2 mb-8">
|
|
97
|
+
<BarChart3 size={28} className="text-primary-600" />
|
|
98
|
+
<h1 className="text-3xl font-bold">Statistiques d'inscriptions</h1>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Key Metrics Cards */}
|
|
102
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-12">
|
|
103
|
+
{/* Total Signups */}
|
|
104
|
+
<Card className="bg-content2 border border-primary-200 dark:border-primary-800">
|
|
105
|
+
<CardBody className="gap-4">
|
|
106
|
+
<div className="flex items-center justify-between">
|
|
107
|
+
<span className="text-sm font-medium text-primary-600 dark:text-primary-400">
|
|
108
|
+
Total d'inscriptions
|
|
109
|
+
</span>
|
|
110
|
+
<TrendingUp size={20} className="text-primary-600" />
|
|
111
|
+
</div>
|
|
112
|
+
<p className="text-4xl font-bold text-primary-700 dark:text-primary-300">
|
|
113
|
+
{stats.total}
|
|
114
|
+
</p>
|
|
115
|
+
</CardBody>
|
|
116
|
+
</Card>
|
|
117
|
+
|
|
118
|
+
{/* LastBrain Signups */}
|
|
119
|
+
<Card className="bg-content2 border border-secondary-200 dark:border-secondary-800">
|
|
120
|
+
<CardBody className="gap-4">
|
|
121
|
+
<div className="flex items-center justify-between">
|
|
122
|
+
<span className="text-sm font-medium text-secondary-600 dark:text-secondary-400">
|
|
123
|
+
Inscriptions LastBrain
|
|
124
|
+
</span>
|
|
125
|
+
</div>
|
|
126
|
+
<div className="flex items-end gap-2">
|
|
127
|
+
<p className="text-4xl font-bold text-secondary-700 dark:text-secondary-300">
|
|
128
|
+
{stats.bySource.lastbrain}
|
|
129
|
+
</p>
|
|
130
|
+
<Chip size="sm" color="secondary" variant="flat">
|
|
131
|
+
{lastbrainPercentage}%
|
|
132
|
+
</Chip>
|
|
133
|
+
</div>
|
|
134
|
+
</CardBody>
|
|
135
|
+
</Card>
|
|
136
|
+
|
|
137
|
+
{/* Recipe Signups */}
|
|
138
|
+
<Card className="bg-content2 border border-success-200 dark:border-success-800">
|
|
139
|
+
<CardBody className="gap-4">
|
|
140
|
+
<div className="flex items-center justify-between">
|
|
141
|
+
<span className="text-sm font-medium text-success-600 dark:text-success-400">
|
|
142
|
+
Inscriptions Recipe
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
<div className="flex items-end gap-2">
|
|
146
|
+
<p className="text-4xl font-bold text-success-700 dark:text-success-300">
|
|
147
|
+
{stats.bySource.recipe}
|
|
148
|
+
</p>
|
|
149
|
+
<Chip size="sm" color="success" variant="flat">
|
|
150
|
+
{recipePercentage}%
|
|
151
|
+
</Chip>
|
|
152
|
+
</div>
|
|
153
|
+
</CardBody>
|
|
154
|
+
</Card>
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{/* Tabs for different views */}
|
|
158
|
+
<Tabs
|
|
159
|
+
aria-label="Vues des statistiques"
|
|
160
|
+
color="primary"
|
|
161
|
+
variant="bordered"
|
|
162
|
+
>
|
|
163
|
+
{/* By Source Tab */}
|
|
164
|
+
<Tab key="by-source" title="Par Source">
|
|
165
|
+
<Card className="mt-6">
|
|
166
|
+
<CardHeader>
|
|
167
|
+
<h3 className="text-lg font-semibold">Résumé par source</h3>
|
|
168
|
+
</CardHeader>
|
|
169
|
+
<CardBody>
|
|
170
|
+
<div className="space-y-4">
|
|
171
|
+
{/* LastBrain */}
|
|
172
|
+
<div className="p-4 rounded-lg border border-secondary-800 dark:border-secondary-800">
|
|
173
|
+
<div className="flex items-center justify-between mb-2">
|
|
174
|
+
<span className="font-medium text-secondary-700 dark:text-secondary-300">
|
|
175
|
+
LastBrain
|
|
176
|
+
</span>
|
|
177
|
+
<Chip size="sm" color="secondary" variant="flat">
|
|
178
|
+
{stats.bySource.lastbrain} inscriptions
|
|
179
|
+
</Chip>
|
|
180
|
+
</div>
|
|
181
|
+
<div className="w-full bg-default-200 rounded-full h-2 dark:bg-default-700">
|
|
182
|
+
<div
|
|
183
|
+
className="bg-secondary-500 h-2 rounded-full"
|
|
184
|
+
style={{
|
|
185
|
+
width: `${(stats.bySource.lastbrain / stats.total) * 100}%`,
|
|
186
|
+
}}
|
|
187
|
+
/>
|
|
188
|
+
</div>
|
|
189
|
+
<p className="text-xs text-default-500 mt-2">
|
|
190
|
+
{lastbrainPercentage}% du total
|
|
191
|
+
</p>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{/* Recipe */}
|
|
195
|
+
<div className="p-4 rounded-lg border border-success-200 dark:border-success-800">
|
|
196
|
+
<div className="flex items-center justify-between mb-2">
|
|
197
|
+
<span className="font-medium text-success-700 dark:text-success-300">
|
|
198
|
+
Recipe
|
|
199
|
+
</span>
|
|
200
|
+
<Chip size="sm" color="success" variant="flat">
|
|
201
|
+
{stats.bySource.recipe} inscriptions
|
|
202
|
+
</Chip>
|
|
203
|
+
</div>
|
|
204
|
+
<div className="w-full bg-default-200 rounded-full h-2 dark:bg-default-700">
|
|
205
|
+
<div
|
|
206
|
+
className="bg-success-500 h-2 rounded-full"
|
|
207
|
+
style={{
|
|
208
|
+
width: `${(stats.bySource.recipe / stats.total) * 100}%`,
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
</div>
|
|
212
|
+
<p className="text-xs text-default-500 mt-2">
|
|
213
|
+
{recipePercentage}% du total
|
|
214
|
+
</p>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
</CardBody>
|
|
218
|
+
</Card>
|
|
219
|
+
</Tab>
|
|
220
|
+
|
|
221
|
+
{/* By Date Tab */}
|
|
222
|
+
<Tab key="by-date" title="Par Date (30 derniers jours)">
|
|
223
|
+
<Card className="mt-6">
|
|
224
|
+
<CardHeader>
|
|
225
|
+
<h3 className="text-lg font-semibold">Inscriptions par date</h3>
|
|
226
|
+
<p className="text-sm text-default-500">
|
|
227
|
+
Derniers 30 jours avec répartition par source
|
|
228
|
+
</p>
|
|
229
|
+
</CardHeader>
|
|
230
|
+
<CardBody>
|
|
231
|
+
<Table
|
|
232
|
+
aria-label="Tableau des inscriptions par date"
|
|
233
|
+
className="rounded-lg"
|
|
234
|
+
>
|
|
235
|
+
<TableHeader>
|
|
236
|
+
<TableColumn>Date</TableColumn>
|
|
237
|
+
<TableColumn className="text-right">LastBrain</TableColumn>
|
|
238
|
+
<TableColumn className="text-right">Recipe</TableColumn>
|
|
239
|
+
<TableColumn className="text-right">Total</TableColumn>
|
|
240
|
+
</TableHeader>
|
|
241
|
+
<TableBody>
|
|
242
|
+
{stats.byDate.map((row) => (
|
|
243
|
+
<TableRow key={row.date}>
|
|
244
|
+
<TableCell>
|
|
245
|
+
<span className="font-medium">
|
|
246
|
+
{new Date(row.date).toLocaleDateString("fr-FR", {
|
|
247
|
+
weekday: "short",
|
|
248
|
+
year: "2-digit",
|
|
249
|
+
month: "2-digit",
|
|
250
|
+
day: "2-digit",
|
|
251
|
+
})}
|
|
252
|
+
</span>
|
|
253
|
+
</TableCell>
|
|
254
|
+
<TableCell className="text-right">
|
|
255
|
+
<Chip
|
|
256
|
+
size="sm"
|
|
257
|
+
color={row.lastbrain > 0 ? "secondary" : "default"}
|
|
258
|
+
variant="flat"
|
|
259
|
+
>
|
|
260
|
+
{row.lastbrain}
|
|
261
|
+
</Chip>
|
|
262
|
+
</TableCell>
|
|
263
|
+
<TableCell className="text-right">
|
|
264
|
+
<Chip
|
|
265
|
+
size="sm"
|
|
266
|
+
color={row.recipe > 0 ? "success" : "default"}
|
|
267
|
+
variant="flat"
|
|
268
|
+
>
|
|
269
|
+
{row.recipe}
|
|
270
|
+
</Chip>
|
|
271
|
+
</TableCell>
|
|
272
|
+
<TableCell className="text-right">
|
|
273
|
+
<span className="font-semibold text-primary-600 dark:text-primary-400">
|
|
274
|
+
{row.total}
|
|
275
|
+
</span>
|
|
276
|
+
</TableCell>
|
|
277
|
+
</TableRow>
|
|
278
|
+
))}
|
|
279
|
+
</TableBody>
|
|
280
|
+
</Table>
|
|
281
|
+
|
|
282
|
+
{stats.byDate.length === 0 && (
|
|
283
|
+
<div className="text-center py-8 text-default-500">
|
|
284
|
+
<p>Aucune donnée disponible pour les 30 derniers jours</p>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</CardBody>
|
|
288
|
+
</Card>
|
|
289
|
+
</Tab>
|
|
290
|
+
</Tabs>
|
|
291
|
+
|
|
292
|
+
{/* Info Card */}
|
|
293
|
+
<Card className="border-l-4 border-l-primary-500 bg-primary-50/30 dark:bg-primary-950/20">
|
|
294
|
+
<CardBody className="py-3">
|
|
295
|
+
<p className="text-sm text-primary-700 dark:text-primary-300">
|
|
296
|
+
<strong>💡 Info:</strong> Ces statistiques vous permettent de suivre
|
|
297
|
+
les inscriptions en fonction de la source (LastBrain ou Recipe) pour
|
|
298
|
+
mieux comprendre d'où proviennent vos utilisateurs.
|
|
299
|
+
</p>
|
|
300
|
+
</CardBody>
|
|
301
|
+
</Card>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
@@ -58,6 +58,7 @@ interface UserProfile {
|
|
|
58
58
|
avatar_url?: string;
|
|
59
59
|
created_at?: string;
|
|
60
60
|
updated_at?: string;
|
|
61
|
+
signup_source?: string;
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -223,6 +224,21 @@ export function UserDetailPage({
|
|
|
223
224
|
>
|
|
224
225
|
{isAdmin ? "Administrateur" : "Utilisateur"}
|
|
225
226
|
</Chip>
|
|
227
|
+
{userProfile.profile?.signup_source && (
|
|
228
|
+
<Chip
|
|
229
|
+
variant="flat"
|
|
230
|
+
color={
|
|
231
|
+
userProfile.profile.signup_source.toLowerCase() === "recipe"
|
|
232
|
+
? "success"
|
|
233
|
+
: "secondary"
|
|
234
|
+
}
|
|
235
|
+
size="sm"
|
|
236
|
+
>
|
|
237
|
+
{userProfile.profile.signup_source.toLowerCase() === "recipe"
|
|
238
|
+
? "Recipe"
|
|
239
|
+
: "LastBrain"}
|
|
240
|
+
</Chip>
|
|
241
|
+
)}
|
|
226
242
|
<Snippet color="default" size="sm" symbol="#">
|
|
227
243
|
{userId}
|
|
228
244
|
</Snippet>
|
|
@@ -414,7 +430,7 @@ export function UserDetailPage({
|
|
|
414
430
|
<p className="font-medium mt-1">
|
|
415
431
|
{userProfile.created_at
|
|
416
432
|
? new Date(
|
|
417
|
-
userProfile.created_at
|
|
433
|
+
userProfile.created_at
|
|
418
434
|
).toLocaleDateString("fr-FR", {
|
|
419
435
|
year: "numeric",
|
|
420
436
|
month: "long",
|
|
@@ -430,7 +446,7 @@ export function UserDetailPage({
|
|
|
430
446
|
<p className="font-medium mt-1">
|
|
431
447
|
{userProfile.last_sign_in_at
|
|
432
448
|
? new Date(
|
|
433
|
-
userProfile.last_sign_in_at
|
|
449
|
+
userProfile.last_sign_in_at
|
|
434
450
|
).toLocaleDateString("fr-FR", {
|
|
435
451
|
year: "numeric",
|
|
436
452
|
month: "long",
|
|
@@ -542,7 +558,7 @@ export function UserDetailPage({
|
|
|
542
558
|
user_metadata: userProfile.raw_user_meta_data,
|
|
543
559
|
},
|
|
544
560
|
null,
|
|
545
|
-
2
|
|
561
|
+
2
|
|
546
562
|
)}
|
|
547
563
|
</pre>
|
|
548
564
|
</div>
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
CardHeader,
|
|
8
|
+
Chip,
|
|
9
|
+
Input,
|
|
10
|
+
Pagination,
|
|
11
|
+
Select,
|
|
12
|
+
SelectItem,
|
|
13
|
+
Spinner,
|
|
14
|
+
Table,
|
|
15
|
+
TableBody,
|
|
16
|
+
TableCell,
|
|
17
|
+
TableColumn,
|
|
18
|
+
TableHeader,
|
|
19
|
+
TableRow,
|
|
20
|
+
} from "@lastbrain/ui";
|
|
21
|
+
import { Search, Users } from "lucide-react";
|
|
22
|
+
|
|
23
|
+
interface User {
|
|
24
|
+
id: string;
|
|
25
|
+
owner_id: string;
|
|
26
|
+
email: string;
|
|
27
|
+
name: string;
|
|
28
|
+
signup_source: string;
|
|
29
|
+
created_at: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface PaginationData {
|
|
33
|
+
page: number;
|
|
34
|
+
limit: number;
|
|
35
|
+
total: number;
|
|
36
|
+
totalPages: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function UsersBySignupSourcePage() {
|
|
40
|
+
const [users, setUsers] = useState<User[]>([]);
|
|
41
|
+
const [loading, setLoading] = useState(true);
|
|
42
|
+
const [error, setError] = useState<string | null>(null);
|
|
43
|
+
const [pagination, setPagination] = useState<PaginationData>({
|
|
44
|
+
page: 1,
|
|
45
|
+
limit: 25,
|
|
46
|
+
total: 0,
|
|
47
|
+
totalPages: 0,
|
|
48
|
+
});
|
|
49
|
+
const [source, setSource] = useState<string>(""); // '' for all, 'lastbrain', 'recipe'
|
|
50
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
fetchUsers(pagination.page, source, searchQuery);
|
|
54
|
+
}, []);
|
|
55
|
+
|
|
56
|
+
const fetchUsers = async (
|
|
57
|
+
page: number,
|
|
58
|
+
selectedSource: string,
|
|
59
|
+
query: string
|
|
60
|
+
) => {
|
|
61
|
+
try {
|
|
62
|
+
setLoading(true);
|
|
63
|
+
|
|
64
|
+
let url = `/api/admin/users-by-source?page=${page}&limit=25`;
|
|
65
|
+
if (selectedSource) {
|
|
66
|
+
url += `&source=${selectedSource}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const response = await fetch(url);
|
|
70
|
+
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error("Erreur lors du chargement des utilisateurs");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await response.json();
|
|
76
|
+
|
|
77
|
+
// Filter by search query if provided
|
|
78
|
+
let filteredUsers = result.data;
|
|
79
|
+
if (query) {
|
|
80
|
+
filteredUsers = filteredUsers.filter(
|
|
81
|
+
(user: User) =>
|
|
82
|
+
user.email.toLowerCase().includes(query.toLowerCase()) ||
|
|
83
|
+
user.name.toLowerCase().includes(query.toLowerCase())
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setUsers(filteredUsers);
|
|
88
|
+
setPagination(result.pagination);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setError(
|
|
91
|
+
err instanceof Error ? err.message : "Erreur lors du chargement"
|
|
92
|
+
);
|
|
93
|
+
} finally {
|
|
94
|
+
setLoading(false);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handleSourceChange = (value: string) => {
|
|
99
|
+
setSource(value);
|
|
100
|
+
fetchUsers(1, value, searchQuery);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleSearch = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
104
|
+
const query = e.target.value;
|
|
105
|
+
setSearchQuery(query);
|
|
106
|
+
fetchUsers(1, source, query);
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handlePageChange = (page: number) => {
|
|
110
|
+
fetchUsers(page, source, searchQuery);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const getSourceColor = (src: string) => {
|
|
114
|
+
return src.toLowerCase() === "recipe" ? "success" : "secondary";
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (error) {
|
|
118
|
+
return (
|
|
119
|
+
<div className="p-6">
|
|
120
|
+
<Card className="border border-danger-200 bg-danger-50/50">
|
|
121
|
+
<CardBody>
|
|
122
|
+
<p className="text-danger-600">{error}</p>
|
|
123
|
+
</CardBody>
|
|
124
|
+
</Card>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div className="space-y-6 p-6">
|
|
131
|
+
{/* Header */}
|
|
132
|
+
<div className="flex items-center gap-2 mb-8">
|
|
133
|
+
<Users size={28} className="text-primary-600" />
|
|
134
|
+
<h1 className="text-3xl font-bold">
|
|
135
|
+
Utilisateurs par source d'inscription
|
|
136
|
+
</h1>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Filters */}
|
|
140
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
141
|
+
<Input
|
|
142
|
+
placeholder="Rechercher par email ou nom..."
|
|
143
|
+
value={searchQuery}
|
|
144
|
+
onChange={handleSearch}
|
|
145
|
+
startContent={<Search size={16} />}
|
|
146
|
+
isClearable
|
|
147
|
+
onClear={() => {
|
|
148
|
+
setSearchQuery("");
|
|
149
|
+
fetchUsers(1, source, "");
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
|
|
153
|
+
<Select
|
|
154
|
+
label="Filtrer par source"
|
|
155
|
+
selectedKeys={[source]}
|
|
156
|
+
onChange={(e) => handleSourceChange(e.target.value)}
|
|
157
|
+
>
|
|
158
|
+
<SelectItem key="">Toutes les sources</SelectItem>
|
|
159
|
+
<SelectItem key="lastbrain">LastBrain</SelectItem>
|
|
160
|
+
<SelectItem key="recipe">Recipe</SelectItem>
|
|
161
|
+
</Select>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
{/* Results info */}
|
|
165
|
+
<div className="text-sm text-default-600">
|
|
166
|
+
Affichage de{" "}
|
|
167
|
+
<span className="font-semibold">
|
|
168
|
+
{(pagination.page - 1) * pagination.limit + 1}-
|
|
169
|
+
{Math.min(pagination.page * pagination.limit, pagination.total)}
|
|
170
|
+
</span>{" "}
|
|
171
|
+
sur <span className="font-semibold">{pagination.total}</span>{" "}
|
|
172
|
+
utilisateurs
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
{/* Users Table */}
|
|
176
|
+
<Card>
|
|
177
|
+
<CardHeader>
|
|
178
|
+
<h3 className="text-lg font-semibold">Liste des utilisateurs</h3>
|
|
179
|
+
</CardHeader>
|
|
180
|
+
<CardBody>
|
|
181
|
+
{loading ? (
|
|
182
|
+
<div className="flex justify-center py-8">
|
|
183
|
+
<Spinner size="lg" label="Chargement..." />
|
|
184
|
+
</div>
|
|
185
|
+
) : (
|
|
186
|
+
<>
|
|
187
|
+
<Table aria-label="Tableau des utilisateurs">
|
|
188
|
+
<TableHeader>
|
|
189
|
+
<TableColumn>Nom</TableColumn>
|
|
190
|
+
<TableColumn>Email</TableColumn>
|
|
191
|
+
<TableColumn>Source</TableColumn>
|
|
192
|
+
<TableColumn>Date d'inscription</TableColumn>
|
|
193
|
+
</TableHeader>
|
|
194
|
+
<TableBody>
|
|
195
|
+
{users.length > 0 ? (
|
|
196
|
+
users.map((user) => (
|
|
197
|
+
<TableRow key={user.id}>
|
|
198
|
+
<TableCell>
|
|
199
|
+
<span className="font-medium">{user.name}</span>
|
|
200
|
+
</TableCell>
|
|
201
|
+
<TableCell>
|
|
202
|
+
<span className="text-sm text-default-600">
|
|
203
|
+
{user.email}
|
|
204
|
+
</span>
|
|
205
|
+
</TableCell>
|
|
206
|
+
<TableCell>
|
|
207
|
+
<Chip
|
|
208
|
+
size="sm"
|
|
209
|
+
color={getSourceColor(user.signup_source)}
|
|
210
|
+
variant="flat"
|
|
211
|
+
>
|
|
212
|
+
{user.signup_source.toLowerCase() === "recipe"
|
|
213
|
+
? "🍳 Recipe"
|
|
214
|
+
: "🧠 LastBrain"}
|
|
215
|
+
</Chip>
|
|
216
|
+
</TableCell>
|
|
217
|
+
<TableCell>
|
|
218
|
+
<span className="text-sm text-default-500">
|
|
219
|
+
{new Date(user.created_at).toLocaleDateString(
|
|
220
|
+
"fr-FR",
|
|
221
|
+
{
|
|
222
|
+
year: "2-digit",
|
|
223
|
+
month: "2-digit",
|
|
224
|
+
day: "2-digit",
|
|
225
|
+
hour: "2-digit",
|
|
226
|
+
minute: "2-digit",
|
|
227
|
+
}
|
|
228
|
+
)}
|
|
229
|
+
</span>
|
|
230
|
+
</TableCell>
|
|
231
|
+
</TableRow>
|
|
232
|
+
))
|
|
233
|
+
) : (
|
|
234
|
+
<TableRow>
|
|
235
|
+
<TableCell colSpan={4} className="text-center py-8">
|
|
236
|
+
<p className="text-default-500">
|
|
237
|
+
Aucun utilisateur trouvé
|
|
238
|
+
</p>
|
|
239
|
+
</TableCell>
|
|
240
|
+
</TableRow>
|
|
241
|
+
)}
|
|
242
|
+
</TableBody>
|
|
243
|
+
</Table>
|
|
244
|
+
|
|
245
|
+
{/* Pagination */}
|
|
246
|
+
{pagination.totalPages > 1 && (
|
|
247
|
+
<div className="flex justify-center mt-6">
|
|
248
|
+
<Pagination
|
|
249
|
+
total={pagination.totalPages}
|
|
250
|
+
page={pagination.page}
|
|
251
|
+
onChange={handlePageChange}
|
|
252
|
+
showControls
|
|
253
|
+
/>
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
</>
|
|
257
|
+
)}
|
|
258
|
+
</CardBody>
|
|
259
|
+
</Card>
|
|
260
|
+
</div>
|
|
261
|
+
);
|
|
262
|
+
}
|
package/src/web/admin/users.tsx
CHANGED
|
@@ -196,7 +196,7 @@ export function DashboardPage() {
|
|
|
196
196
|
<p className="text-2xl font-bold text-secondary">
|
|
197
197
|
{Math.floor(
|
|
198
198
|
(Date.now() - new Date(userData.created_at).getTime()) /
|
|
199
|
-
(1000 * 60 * 60 * 24)
|
|
199
|
+
(1000 * 60 * 60 * 24)
|
|
200
200
|
)}
|
|
201
201
|
</p>
|
|
202
202
|
<p className="text-small text-default-500">Days active</p>
|
package/src/web/auth/folder.tsx
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import {
|
|
5
|
-
Card,
|
|
6
|
-
CardBody,
|
|
7
|
-
CardHeader,
|
|
8
|
-
Spinner,
|
|
9
|
-
Chip,
|
|
10
|
-
Divider,
|
|
11
|
-
Avatar,
|
|
12
|
-
} from "@lastbrain/ui";
|
|
4
|
+
import { Card, CardBody, Spinner } from "@lastbrain/ui";
|
|
13
5
|
import { FileManager } from "@lastbrain/ui";
|
|
6
|
+
import { ImageGenerative } from "@lastbrain/module-ai";
|
|
14
7
|
|
|
15
8
|
interface UserData {
|
|
16
9
|
id: string;
|
|
@@ -80,11 +73,6 @@ export function FolderPage() {
|
|
|
80
73
|
return null;
|
|
81
74
|
}
|
|
82
75
|
|
|
83
|
-
const fullName =
|
|
84
|
-
userData.profile?.first_name && userData.profile?.last_name
|
|
85
|
-
? `${userData.profile.first_name} ${userData.profile.last_name}`
|
|
86
|
-
: "User";
|
|
87
|
-
|
|
88
76
|
return (
|
|
89
77
|
<div className="pt-4 pb-12 max-w-8xl mx-auto px-4">
|
|
90
78
|
<h1 className="text-3xl font-bold mb-8">Dossier</h1>
|
|
@@ -94,6 +82,8 @@ export function FolderPage() {
|
|
|
94
82
|
basePath={userData.id}
|
|
95
83
|
allowUpload={true}
|
|
96
84
|
allowCreateFolder={true}
|
|
85
|
+
allowAIImageGeneration={true}
|
|
86
|
+
renderImageGenerator={(props) => <ImageGenerative {...props} />}
|
|
97
87
|
className="min-h-[80vh]"
|
|
98
88
|
/>
|
|
99
89
|
</div>
|