@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,304 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import {
|
|
5
|
+
Card,
|
|
6
|
+
CardBody,
|
|
7
|
+
CardHeader,
|
|
8
|
+
Switch,
|
|
9
|
+
Button,
|
|
10
|
+
Spinner,
|
|
11
|
+
Divider,
|
|
12
|
+
Select,
|
|
13
|
+
SelectItem,
|
|
14
|
+
addToast,
|
|
15
|
+
} from "@lastbrain/ui";
|
|
16
|
+
import { Settings, Save } from "lucide-react";
|
|
17
|
+
|
|
18
|
+
interface UserPreferences {
|
|
19
|
+
email_notifications?: boolean;
|
|
20
|
+
push_notifications?: boolean;
|
|
21
|
+
marketing_emails?: boolean;
|
|
22
|
+
theme?: string;
|
|
23
|
+
language?: string;
|
|
24
|
+
timezone?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ProfileData {
|
|
28
|
+
language?: string;
|
|
29
|
+
timezone?: string;
|
|
30
|
+
preferences?: UserPreferences;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ReglagePage() {
|
|
34
|
+
const [preferences, setPreferences] = useState<UserPreferences>({
|
|
35
|
+
email_notifications: true,
|
|
36
|
+
push_notifications: false,
|
|
37
|
+
marketing_emails: false,
|
|
38
|
+
theme: "system",
|
|
39
|
+
language: "en",
|
|
40
|
+
timezone: "UTC",
|
|
41
|
+
});
|
|
42
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
43
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
fetchSettings();
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
const fetchSettings = async () => {
|
|
50
|
+
try {
|
|
51
|
+
setIsLoading(true);
|
|
52
|
+
const response = await fetch("/api/auth/profile");
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error("Failed to fetch settings");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = await response.json();
|
|
59
|
+
if (result.data) {
|
|
60
|
+
const profile: ProfileData = result.data;
|
|
61
|
+
setPreferences((prev) => ({
|
|
62
|
+
...prev,
|
|
63
|
+
language: profile.language || prev.language,
|
|
64
|
+
timezone: profile.timezone || prev.timezone,
|
|
65
|
+
...(profile.preferences || {}),
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("Error loading settings:", err);
|
|
70
|
+
addToast({
|
|
71
|
+
title: "Error",
|
|
72
|
+
description: "Failed to load settings",
|
|
73
|
+
color: "danger",
|
|
74
|
+
});
|
|
75
|
+
} finally {
|
|
76
|
+
setIsLoading(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleSave = async () => {
|
|
81
|
+
setIsSaving(true);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch("/api/auth/profile", {
|
|
85
|
+
method: "PUT",
|
|
86
|
+
headers: {
|
|
87
|
+
"Content-Type": "application/json",
|
|
88
|
+
},
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
language: preferences.language,
|
|
91
|
+
timezone: preferences.timezone,
|
|
92
|
+
preferences: {
|
|
93
|
+
email_notifications: preferences.email_notifications,
|
|
94
|
+
push_notifications: preferences.push_notifications,
|
|
95
|
+
marketing_emails: preferences.marketing_emails,
|
|
96
|
+
theme: preferences.theme,
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
throw new Error("Failed to update settings");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
addToast({
|
|
106
|
+
title: "Success",
|
|
107
|
+
description: "Settings updated successfully",
|
|
108
|
+
color: "success",
|
|
109
|
+
});
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.error("Error updating settings:", err);
|
|
112
|
+
addToast({
|
|
113
|
+
title: "Error",
|
|
114
|
+
description: "Failed to update settings",
|
|
115
|
+
color: "danger",
|
|
116
|
+
});
|
|
117
|
+
} finally {
|
|
118
|
+
setIsSaving(false);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const handleToggle = (key: keyof UserPreferences, value: boolean) => {
|
|
123
|
+
setPreferences((prev) => ({ ...prev, [key]: value }));
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const handleSelect = (key: keyof UserPreferences, value: string) => {
|
|
127
|
+
setPreferences((prev) => ({ ...prev, [key]: value }));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (isLoading) {
|
|
131
|
+
return (
|
|
132
|
+
<div className="flex justify-center items-center min-h-[400px]">
|
|
133
|
+
<Spinner size="lg" label="Loading settings..." />
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<div className="pt-12 pb-12 max-w-4xl mx-auto px-4">
|
|
140
|
+
<div className="flex items-center gap-2 mb-8">
|
|
141
|
+
<Settings className="w-8 h-8" />
|
|
142
|
+
<h1 className="text-3xl font-bold">Account Settings</h1>
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="space-y-6">
|
|
146
|
+
{/* Notifications */}
|
|
147
|
+
<Card>
|
|
148
|
+
<CardHeader>
|
|
149
|
+
<h3 className="text-lg font-semibold">Notifications</h3>
|
|
150
|
+
</CardHeader>
|
|
151
|
+
<Divider />
|
|
152
|
+
<CardBody>
|
|
153
|
+
<div className="space-y-4">
|
|
154
|
+
<div className="flex justify-between items-center">
|
|
155
|
+
<div>
|
|
156
|
+
<p className="font-medium">Email Notifications</p>
|
|
157
|
+
<p className="text-small text-default-500">
|
|
158
|
+
Receive email notifications for important updates
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
<Switch
|
|
162
|
+
isSelected={preferences.email_notifications}
|
|
163
|
+
onValueChange={(value) =>
|
|
164
|
+
handleToggle("email_notifications", value)
|
|
165
|
+
}
|
|
166
|
+
/>
|
|
167
|
+
</div>
|
|
168
|
+
<Divider />
|
|
169
|
+
<div className="flex justify-between items-center">
|
|
170
|
+
<div>
|
|
171
|
+
<p className="font-medium">Push Notifications</p>
|
|
172
|
+
<p className="text-small text-default-500">
|
|
173
|
+
Receive push notifications in your browser
|
|
174
|
+
</p>
|
|
175
|
+
</div>
|
|
176
|
+
<Switch
|
|
177
|
+
isSelected={preferences.push_notifications}
|
|
178
|
+
onValueChange={(value) =>
|
|
179
|
+
handleToggle("push_notifications", value)
|
|
180
|
+
}
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
<Divider />
|
|
184
|
+
<div className="flex justify-between items-center">
|
|
185
|
+
<div>
|
|
186
|
+
<p className="font-medium">Marketing Emails</p>
|
|
187
|
+
<p className="text-small text-default-500">
|
|
188
|
+
Receive emails about new features and updates
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
<Switch
|
|
192
|
+
isSelected={preferences.marketing_emails}
|
|
193
|
+
onValueChange={(value) =>
|
|
194
|
+
handleToggle("marketing_emails", value)
|
|
195
|
+
}
|
|
196
|
+
/>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</CardBody>
|
|
200
|
+
</Card>
|
|
201
|
+
|
|
202
|
+
{/* Appearance */}
|
|
203
|
+
<Card>
|
|
204
|
+
<CardHeader>
|
|
205
|
+
<h3 className="text-lg font-semibold">Appearance</h3>
|
|
206
|
+
</CardHeader>
|
|
207
|
+
<Divider />
|
|
208
|
+
<CardBody>
|
|
209
|
+
<Select
|
|
210
|
+
label="Theme"
|
|
211
|
+
placeholder="Select a theme"
|
|
212
|
+
selectedKeys={preferences.theme ? [preferences.theme] : []}
|
|
213
|
+
onChange={(e) => handleSelect("theme", e.target.value)}
|
|
214
|
+
>
|
|
215
|
+
<SelectItem key="light">
|
|
216
|
+
Light
|
|
217
|
+
</SelectItem>
|
|
218
|
+
<SelectItem key="dark">
|
|
219
|
+
Dark
|
|
220
|
+
</SelectItem>
|
|
221
|
+
<SelectItem key="system">
|
|
222
|
+
System
|
|
223
|
+
</SelectItem>
|
|
224
|
+
</Select>
|
|
225
|
+
</CardBody>
|
|
226
|
+
</Card>
|
|
227
|
+
|
|
228
|
+
{/* Language & Region */}
|
|
229
|
+
<Card>
|
|
230
|
+
<CardHeader>
|
|
231
|
+
<h3 className="text-lg font-semibold">Language & Region</h3>
|
|
232
|
+
</CardHeader>
|
|
233
|
+
<Divider />
|
|
234
|
+
<CardBody>
|
|
235
|
+
<div className="grid gap-4 md:grid-cols-2">
|
|
236
|
+
<Select
|
|
237
|
+
label="Language"
|
|
238
|
+
placeholder="Select a language"
|
|
239
|
+
selectedKeys={preferences.language ? [preferences.language] : []}
|
|
240
|
+
onChange={(e) => handleSelect("language", e.target.value)}
|
|
241
|
+
>
|
|
242
|
+
<SelectItem key="en">
|
|
243
|
+
English
|
|
244
|
+
</SelectItem>
|
|
245
|
+
<SelectItem key="fr">
|
|
246
|
+
Français
|
|
247
|
+
</SelectItem>
|
|
248
|
+
<SelectItem key="es">
|
|
249
|
+
Español
|
|
250
|
+
</SelectItem>
|
|
251
|
+
<SelectItem key="de">
|
|
252
|
+
Deutsch
|
|
253
|
+
</SelectItem>
|
|
254
|
+
</Select>
|
|
255
|
+
<Select
|
|
256
|
+
label="Timezone"
|
|
257
|
+
placeholder="Select a timezone"
|
|
258
|
+
selectedKeys={preferences.timezone ? [preferences.timezone] : []}
|
|
259
|
+
onChange={(e) => handleSelect("timezone", e.target.value)}
|
|
260
|
+
>
|
|
261
|
+
<SelectItem key="UTC">
|
|
262
|
+
UTC
|
|
263
|
+
</SelectItem>
|
|
264
|
+
<SelectItem key="Europe/Paris">
|
|
265
|
+
Europe/Paris
|
|
266
|
+
</SelectItem>
|
|
267
|
+
<SelectItem key="America/New_York">
|
|
268
|
+
America/New_York
|
|
269
|
+
</SelectItem>
|
|
270
|
+
<SelectItem key="America/Los_Angeles">
|
|
271
|
+
America/Los_Angeles
|
|
272
|
+
</SelectItem>
|
|
273
|
+
<SelectItem key="Asia/Tokyo">
|
|
274
|
+
Asia/Tokyo
|
|
275
|
+
</SelectItem>
|
|
276
|
+
</Select>
|
|
277
|
+
</div>
|
|
278
|
+
</CardBody>
|
|
279
|
+
</Card>
|
|
280
|
+
|
|
281
|
+
{/* Actions */}
|
|
282
|
+
<div className="flex justify-end gap-3">
|
|
283
|
+
<Button
|
|
284
|
+
type="button"
|
|
285
|
+
variant="flat"
|
|
286
|
+
onPress={() => fetchSettings()}
|
|
287
|
+
isDisabled={isSaving}
|
|
288
|
+
>
|
|
289
|
+
Reset
|
|
290
|
+
</Button>
|
|
291
|
+
<Button
|
|
292
|
+
type="button"
|
|
293
|
+
color="primary"
|
|
294
|
+
isLoading={isSaving}
|
|
295
|
+
onPress={handleSave}
|
|
296
|
+
startContent={!isSaving && <Save className="w-4 h-4" />}
|
|
297
|
+
>
|
|
298
|
+
{isSaving ? "Saving..." : "Save Settings"}
|
|
299
|
+
</Button>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { FormEvent } from "react";
|
|
4
|
+
import { Suspense, useState } from "react";
|
|
5
|
+
import { supabaseBrowserClient } from "@lastbrain/core";
|
|
6
|
+
import {
|
|
7
|
+
addToast,
|
|
8
|
+
Button,
|
|
9
|
+
Card,
|
|
10
|
+
CardBody,
|
|
11
|
+
CardHeader,
|
|
12
|
+
Chip,
|
|
13
|
+
Input,
|
|
14
|
+
Link,
|
|
15
|
+
} from "@lastbrain/ui";
|
|
16
|
+
import { ArrowRight, Lock, Mail, Sparkles } from "lucide-react";
|
|
17
|
+
import { useRouter, useSearchParams } from "next/navigation";
|
|
18
|
+
|
|
19
|
+
function SignInForm() {
|
|
20
|
+
const searchParams = useSearchParams();
|
|
21
|
+
|
|
22
|
+
const [email, setEmail] = useState("");
|
|
23
|
+
const [password, setPassword] = useState("");
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
+
const [error, setError] = useState<string | null>(null);
|
|
26
|
+
const redirectUrl = searchParams.get("redirect");
|
|
27
|
+
const router = useRouter();
|
|
28
|
+
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
29
|
+
event.preventDefault();
|
|
30
|
+
setIsLoading(true);
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const { data, error } =
|
|
34
|
+
await supabaseBrowserClient.auth.signInWithPassword({
|
|
35
|
+
email,
|
|
36
|
+
password,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (error) {
|
|
40
|
+
addToast({
|
|
41
|
+
color: "danger",
|
|
42
|
+
title: "Erreur de connexion",
|
|
43
|
+
description: error.message,
|
|
44
|
+
});
|
|
45
|
+
setIsLoading(false);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
setIsLoading(false);
|
|
50
|
+
console.log("Signed in user:", data.user);
|
|
51
|
+
addToast({
|
|
52
|
+
color: "success",
|
|
53
|
+
title: "Connecté avec succès !",
|
|
54
|
+
description: `Bienvenue ${data.user?.email}`,
|
|
55
|
+
});
|
|
56
|
+
router.push(redirectUrl || "/auth/dashboard");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
setIsLoading(false);
|
|
59
|
+
addToast({
|
|
60
|
+
color: "danger",
|
|
61
|
+
title: "Erreur de connexion",
|
|
62
|
+
description: `${err instanceof Error ? err.message : String(err)}`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// return (
|
|
68
|
+
// <div className=" min-h-screen h-full flex flex-col justify-center items-center p-4">
|
|
69
|
+
// <Card className="w-full max-w-md">
|
|
70
|
+
// <CardHeader className="flex flex-col">
|
|
71
|
+
// <h1 className="text-2xl font-semibold text-slate-900 dark:text-white">
|
|
72
|
+
// Connexion
|
|
73
|
+
// </h1>
|
|
74
|
+
// <p className="mt-1 text-sm text-slate-500">
|
|
75
|
+
// Utilisez votre adresse email pour vous connecter.
|
|
76
|
+
// </p>
|
|
77
|
+
// </CardHeader>
|
|
78
|
+
// <CardBody>
|
|
79
|
+
// <form onSubmit={handleSubmit}>
|
|
80
|
+
// <div className="space-y-6">
|
|
81
|
+
// <Input
|
|
82
|
+
// label="Email"
|
|
83
|
+
// type="email"
|
|
84
|
+
// className="mb-6"
|
|
85
|
+
// required
|
|
86
|
+
// value={email}
|
|
87
|
+
// onChange={(event) => setEmail(event.target.value)}
|
|
88
|
+
// />
|
|
89
|
+
|
|
90
|
+
// <Input
|
|
91
|
+
// label="Mot de passe"
|
|
92
|
+
// type="password"
|
|
93
|
+
// required
|
|
94
|
+
// placeholder="Password"
|
|
95
|
+
// className="mb-6"
|
|
96
|
+
// value={password}
|
|
97
|
+
// onChange={(event) => setPassword(event.target.value)}
|
|
98
|
+
// />
|
|
99
|
+
|
|
100
|
+
// <Button
|
|
101
|
+
// size="lg"
|
|
102
|
+
// type="submit"
|
|
103
|
+
// className="w-full"
|
|
104
|
+
// isLoading={isLoading}
|
|
105
|
+
// >
|
|
106
|
+
// Se connecter
|
|
107
|
+
// </Button>
|
|
108
|
+
// </div>
|
|
109
|
+
// </form>
|
|
110
|
+
// </CardBody>
|
|
111
|
+
// </Card>
|
|
112
|
+
// </div>
|
|
113
|
+
// );
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className="pt-12 relative min-h-screen w-full overflow-hidden bg-gradient-to-br from-primary-50/30 to-secondary-50/30 dark:from-primary-950/50 dark:to-secondary-950/50">
|
|
117
|
+
{/* Background decoration */}
|
|
118
|
+
<div className="absolute inset-0 bg-grid-slate-100 dark:bg-grid-slate-800/20 mask-[linear-gradient(0deg,white,rgba(255,255,255,0.6))] dark:mask-[linear-gradient(0deg,rgba(255,255,255,0.1),rgba(255,255,255,0.05))]" />
|
|
119
|
+
|
|
120
|
+
<div className="relative flex min-h-[60vh] items-center justify-center px-4 py-12">
|
|
121
|
+
<div className="w-full max-w-md">
|
|
122
|
+
{/* Header */}
|
|
123
|
+
<div className="mb-8 text-center">
|
|
124
|
+
<Chip
|
|
125
|
+
color="primary"
|
|
126
|
+
variant="flat"
|
|
127
|
+
startContent={<Sparkles className="w-4 h-4" />}
|
|
128
|
+
className="mb-4 p-2"
|
|
129
|
+
>
|
|
130
|
+
Espace membre
|
|
131
|
+
</Chip>
|
|
132
|
+
<h1 className="mb-3 text-4xl font-bold">
|
|
133
|
+
<span className="bg-gradient-to-r from-primary-600 to-secondary-600 bg-clip-text text-transparent">
|
|
134
|
+
Bon retour
|
|
135
|
+
</span>
|
|
136
|
+
</h1>
|
|
137
|
+
<p className="text-default-600 dark:text-default-400">
|
|
138
|
+
Connectez-vous pour accéder à votre espace
|
|
139
|
+
</p>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
{/* Form Card */}
|
|
143
|
+
<Card className="border border-default-200/60 bg-background/60 backdrop-blur-md backdrop-saturate-150 shadow-xl">
|
|
144
|
+
<CardBody className="gap-6 p-8">
|
|
145
|
+
<form onSubmit={handleSubmit} className="flex flex-col gap-6">
|
|
146
|
+
<Input
|
|
147
|
+
label="Email"
|
|
148
|
+
type="email"
|
|
149
|
+
placeholder="votre@email.com"
|
|
150
|
+
value={email}
|
|
151
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
152
|
+
required
|
|
153
|
+
startContent={<Mail className="h-4 w-4 text-default-400" />}
|
|
154
|
+
classNames={{
|
|
155
|
+
input: "text-base",
|
|
156
|
+
inputWrapper:
|
|
157
|
+
"border border-default-200/60 hover:border-primary-400 focus-within:border-primary-500",
|
|
158
|
+
}}
|
|
159
|
+
/>
|
|
160
|
+
|
|
161
|
+
<div className="flex flex-col gap-2">
|
|
162
|
+
<Input
|
|
163
|
+
label="Mot de passe"
|
|
164
|
+
type="password"
|
|
165
|
+
placeholder="••••••••"
|
|
166
|
+
value={password}
|
|
167
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
168
|
+
required
|
|
169
|
+
minLength={6}
|
|
170
|
+
startContent={<Lock className="h-4 w-4 text-default-400" />}
|
|
171
|
+
classNames={{
|
|
172
|
+
input: "text-base",
|
|
173
|
+
inputWrapper:
|
|
174
|
+
"border border-default-200/60 hover:border-primary-400 focus-within:border-primary-500",
|
|
175
|
+
}}
|
|
176
|
+
/>
|
|
177
|
+
<Link
|
|
178
|
+
href="/forgot-password"
|
|
179
|
+
className="text-xs text-default-500 hover:text-primary-500 self-end"
|
|
180
|
+
>
|
|
181
|
+
Mot de passe oublié ?
|
|
182
|
+
</Link>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{error && (
|
|
186
|
+
<div className="rounded-lg border border-danger-200 bg-danger-50/50 px-4 py-3 dark:border-danger-800 dark:bg-danger-950/50">
|
|
187
|
+
<p className="text-sm text-danger-600 dark:text-danger-400">
|
|
188
|
+
{error}
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
<Button
|
|
194
|
+
type="submit"
|
|
195
|
+
color="primary"
|
|
196
|
+
size="lg"
|
|
197
|
+
className="w-full font-semibold"
|
|
198
|
+
endContent={
|
|
199
|
+
isLoading ? null : <ArrowRight className="h-5 w-5" />
|
|
200
|
+
}
|
|
201
|
+
isLoading={isLoading}
|
|
202
|
+
>
|
|
203
|
+
Se connecter
|
|
204
|
+
</Button>
|
|
205
|
+
</form>
|
|
206
|
+
|
|
207
|
+
{/* Divider */}
|
|
208
|
+
<div className="relative flex items-center gap-4 py-2">
|
|
209
|
+
<div className="h-px flex-1 bg-default-200" />
|
|
210
|
+
<span className="text-xs text-default-400">OU</span>
|
|
211
|
+
<div className="h-px flex-1 bg-default-200" />
|
|
212
|
+
</div>
|
|
213
|
+
|
|
214
|
+
{/* Sign up link */}
|
|
215
|
+
<div className="text-center">
|
|
216
|
+
<p className="text-sm text-default-600">
|
|
217
|
+
Pas encore de compte ?{" "}
|
|
218
|
+
<Link
|
|
219
|
+
href={
|
|
220
|
+
redirectUrl
|
|
221
|
+
? `/signup?redirect=${encodeURIComponent(redirectUrl)}`
|
|
222
|
+
: "/signup"
|
|
223
|
+
}
|
|
224
|
+
className="font-semibold text-primary-600 hover:text-primary-700"
|
|
225
|
+
>
|
|
226
|
+
Créer un compte
|
|
227
|
+
</Link>
|
|
228
|
+
</p>
|
|
229
|
+
</div>
|
|
230
|
+
</CardBody>
|
|
231
|
+
</Card>
|
|
232
|
+
|
|
233
|
+
{/* Footer */}
|
|
234
|
+
<p className="mt-8 text-center text-xs text-default-400">
|
|
235
|
+
En vous connectant, vous acceptez nos{" "}
|
|
236
|
+
<Link
|
|
237
|
+
href="/legal/terms"
|
|
238
|
+
className="underline hover:text-primary-500"
|
|
239
|
+
>
|
|
240
|
+
conditions d'utilisation
|
|
241
|
+
</Link>
|
|
242
|
+
</p>
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function SignInPage() {
|
|
250
|
+
return (
|
|
251
|
+
<Suspense fallback={<div>Chargement...</div>}>
|
|
252
|
+
<SignInForm />
|
|
253
|
+
</Suspense>
|
|
254
|
+
);
|
|
255
|
+
}
|