create-crm-tmp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/bin/create-crm-tmp.js +93 -0
  2. package/package.json +25 -0
  3. package/template/.prettierignore +33 -0
  4. package/template/.prettierrc.json +25 -0
  5. package/template/README.md +173 -0
  6. package/template/eslint.config.mjs +18 -0
  7. package/template/exemple-contacts.csv +11 -0
  8. package/template/next.config.ts +8 -0
  9. package/template/package.json +64 -0
  10. package/template/postcss.config.mjs +7 -0
  11. package/template/prisma/migrations/20251126144728_init/migration.sql +78 -0
  12. package/template/prisma/migrations/20251126155204_add_user_roles/migration.sql +5 -0
  13. package/template/prisma/migrations/20251128095126_add_company_info/migration.sql +19 -0
  14. package/template/prisma/migrations/20251128123321_add_smtp_config/migration.sql +22 -0
  15. package/template/prisma/migrations/20251128132303_add_status/migration.sql +23 -0
  16. package/template/prisma/migrations/20251201102207_add_user_active/migration.sql +75 -0
  17. package/template/prisma/migrations/20251201105507_add_email_signature/migration.sql +2 -0
  18. package/template/prisma/migrations/20251201151122_add_tasks/migration.sql +45 -0
  19. package/template/prisma/migrations/20251202111854_add_task_reminder/migration.sql +2 -0
  20. package/template/prisma/migrations/20251202135859_add_google_meet_integration/migration.sql +27 -0
  21. package/template/prisma/migrations/20251203103317_add_meta_lead_integration/migration.sql +20 -0
  22. package/template/prisma/migrations/20251203104002_add_google_ads_integration/migration.sql +18 -0
  23. package/template/prisma/migrations/20251203112122_add_google_sheet_integration/migration.sql +32 -0
  24. package/template/prisma/migrations/20251203153853_allow_multiple_integration_configs/migration.sql +20 -0
  25. package/template/prisma/migrations/20251205141705_update_user_roles/migration.sql +12 -0
  26. package/template/prisma/migrations/20251205150000_add_commercial_and_telepro_assignment/migration.sql +21 -0
  27. package/template/prisma/migrations/20251205160000_add_interaction_logging/migration.sql +11 -0
  28. package/template/prisma/migrations/20251208090314_add_automatic_interaction_types/migration.sql +12 -0
  29. package/template/prisma/migrations/20251208094843_mg/migration.sql +14 -0
  30. package/template/prisma/migrations/20251208100000_add_company_support/migration.sql +14 -0
  31. package/template/prisma/migrations/20251208110000_add_templates/migration.sql +26 -0
  32. package/template/prisma/migrations/20251208141304_add_video_conference_task_type/migration.sql +2 -0
  33. package/template/prisma/migrations/20251209104759_add_internal_note_to_task/migration.sql +2 -0
  34. package/template/prisma/migrations/20251209134803_add_company_field/migration.sql +2 -0
  35. package/template/prisma/migrations/20251209150000_rename_company_to_company_name/migration.sql +3 -0
  36. package/template/prisma/migrations/20251209150016_add_email_tracking/migration.sql +21 -0
  37. package/template/prisma/migrations/20251209155908_add_notify_contact_to_task/migration.sql +2 -0
  38. package/template/prisma/migrations/20251210110019_add_appointment_types/migration.sql +10 -0
  39. package/template/prisma/migrations/20251210113928_add_contact_files/migration.sql +26 -0
  40. package/template/prisma/migrations/20251212132339_add_custom_roles/migration.sql +24 -0
  41. package/template/prisma/migrations/20251215104448_add_file_interaction_types/migration.sql +11 -0
  42. package/template/prisma/migrations/20251215145616_add_closing_reasons/migration.sql +12 -0
  43. package/template/prisma/migrations/20251216140850_add_log_users/migration.sql +25 -0
  44. package/template/prisma/migrations/20251216151000_rename_perdu_to_ferme/migration.sql +8 -0
  45. package/template/prisma/migrations/20251216162318_add_column_mappings_to_google_sheet/migration.sql +2 -0
  46. package/template/prisma/migrations/20251216185127_add_workflows/migration.sql +80 -0
  47. package/template/prisma/migrations/20251216192237_add_scheduled_workflow_actions/migration.sql +32 -0
  48. package/template/prisma/migrations/migration_lock.toml +3 -0
  49. package/template/prisma/schema.prisma +582 -0
  50. package/template/prisma.config.ts +14 -0
  51. package/template/src/app/(auth)/invite/[token]/page.tsx +200 -0
  52. package/template/src/app/(auth)/layout.tsx +3 -0
  53. package/template/src/app/(auth)/reset-password/complete/page.tsx +213 -0
  54. package/template/src/app/(auth)/reset-password/page.tsx +146 -0
  55. package/template/src/app/(auth)/reset-password/verify/page.tsx +183 -0
  56. package/template/src/app/(auth)/signin/page.tsx +166 -0
  57. package/template/src/app/(dashboard)/agenda/page.tsx +3051 -0
  58. package/template/src/app/(dashboard)/automatisation/[id]/page.tsx +24 -0
  59. package/template/src/app/(dashboard)/automatisation/_components/workflow-editor.tsx +905 -0
  60. package/template/src/app/(dashboard)/automatisation/new/page.tsx +20 -0
  61. package/template/src/app/(dashboard)/automatisation/page.tsx +337 -0
  62. package/template/src/app/(dashboard)/closing/page.tsx +1052 -0
  63. package/template/src/app/(dashboard)/contacts/[id]/page.tsx +6028 -0
  64. package/template/src/app/(dashboard)/contacts/page.tsx +3713 -0
  65. package/template/src/app/(dashboard)/dashboard/page.tsx +186 -0
  66. package/template/src/app/(dashboard)/layout.tsx +30 -0
  67. package/template/src/app/(dashboard)/settings/page.tsx +4070 -0
  68. package/template/src/app/(dashboard)/templates/page.tsx +567 -0
  69. package/template/src/app/(dashboard)/users/list/page.tsx +507 -0
  70. package/template/src/app/(dashboard)/users/page.tsx +457 -0
  71. package/template/src/app/(dashboard)/users/permissions/page.tsx +181 -0
  72. package/template/src/app/(dashboard)/users/roles/page.tsx +434 -0
  73. package/template/src/app/api/audit-logs/route.ts +57 -0
  74. package/template/src/app/api/auth/[...all]/route.ts +4 -0
  75. package/template/src/app/api/auth/check-active/route.ts +31 -0
  76. package/template/src/app/api/auth/google/callback/route.ts +94 -0
  77. package/template/src/app/api/auth/google/disconnect/route.ts +32 -0
  78. package/template/src/app/api/auth/google/route.ts +34 -0
  79. package/template/src/app/api/auth/google/status/route.ts +32 -0
  80. package/template/src/app/api/closing-reasons/route.ts +27 -0
  81. package/template/src/app/api/contacts/[id]/files/[fileId]/route.ts +94 -0
  82. package/template/src/app/api/contacts/[id]/files/route.ts +269 -0
  83. package/template/src/app/api/contacts/[id]/interactions/[interactionId]/route.ts +91 -0
  84. package/template/src/app/api/contacts/[id]/interactions/route.ts +103 -0
  85. package/template/src/app/api/contacts/[id]/meet/route.ts +296 -0
  86. package/template/src/app/api/contacts/[id]/route.ts +322 -0
  87. package/template/src/app/api/contacts/[id]/send-email/route.ts +254 -0
  88. package/template/src/app/api/contacts/export/route.ts +270 -0
  89. package/template/src/app/api/contacts/import/route.ts +381 -0
  90. package/template/src/app/api/contacts/route.ts +283 -0
  91. package/template/src/app/api/dashboard/stats/route.ts +299 -0
  92. package/template/src/app/api/email/track/[id]/route.ts +68 -0
  93. package/template/src/app/api/integrations/google-sheet/sync/route.ts +526 -0
  94. package/template/src/app/api/invite/complete/route.ts +88 -0
  95. package/template/src/app/api/invite/validate/route.ts +55 -0
  96. package/template/src/app/api/reminders/route.ts +95 -0
  97. package/template/src/app/api/reset-password/complete/route.ts +73 -0
  98. package/template/src/app/api/reset-password/request/route.ts +84 -0
  99. package/template/src/app/api/reset-password/validate/route.ts +49 -0
  100. package/template/src/app/api/reset-password/verify/route.ts +74 -0
  101. package/template/src/app/api/roles/[id]/route.ts +183 -0
  102. package/template/src/app/api/roles/route.ts +140 -0
  103. package/template/src/app/api/send/route.ts +282 -0
  104. package/template/src/app/api/settings/change-password/route.ts +95 -0
  105. package/template/src/app/api/settings/closing-reasons/[id]/route.ts +84 -0
  106. package/template/src/app/api/settings/closing-reasons/route.ts +74 -0
  107. package/template/src/app/api/settings/company/route.ts +121 -0
  108. package/template/src/app/api/settings/google-ads/[id]/route.ts +117 -0
  109. package/template/src/app/api/settings/google-ads/route.ts +122 -0
  110. package/template/src/app/api/settings/google-sheet/[id]/route.ts +230 -0
  111. package/template/src/app/api/settings/google-sheet/auto-map/route.ts +196 -0
  112. package/template/src/app/api/settings/google-sheet/route.ts +254 -0
  113. package/template/src/app/api/settings/meta-leads/[id]/route.ts +123 -0
  114. package/template/src/app/api/settings/meta-leads/route.ts +132 -0
  115. package/template/src/app/api/settings/profile/route.ts +42 -0
  116. package/template/src/app/api/settings/smtp/route.ts +130 -0
  117. package/template/src/app/api/settings/smtp/test/route.ts +121 -0
  118. package/template/src/app/api/settings/statuses/[id]/route.ts +101 -0
  119. package/template/src/app/api/settings/statuses/route.ts +83 -0
  120. package/template/src/app/api/statuses/route.ts +25 -0
  121. package/template/src/app/api/tasks/[id]/attendees/route.ts +76 -0
  122. package/template/src/app/api/tasks/[id]/route.ts +728 -0
  123. package/template/src/app/api/tasks/meet/route.ts +240 -0
  124. package/template/src/app/api/tasks/route.ts +417 -0
  125. package/template/src/app/api/templates/[id]/route.ts +140 -0
  126. package/template/src/app/api/templates/route.ts +91 -0
  127. package/template/src/app/api/users/[id]/route.ts +168 -0
  128. package/template/src/app/api/users/list/route.ts +45 -0
  129. package/template/src/app/api/users/me/route.ts +48 -0
  130. package/template/src/app/api/users/route.ts +250 -0
  131. package/template/src/app/api/webhooks/google-ads/route.ts +208 -0
  132. package/template/src/app/api/webhooks/meta-leads/route.ts +258 -0
  133. package/template/src/app/api/workflows/[id]/route.ts +192 -0
  134. package/template/src/app/api/workflows/process/route.ts +293 -0
  135. package/template/src/app/api/workflows/route.ts +124 -0
  136. package/template/src/app/favicon.ico +0 -0
  137. package/template/src/app/globals.css +1416 -0
  138. package/template/src/app/layout.tsx +31 -0
  139. package/template/src/app/page.tsx +32 -0
  140. package/template/src/components/dashboard/activity-chart.tsx +67 -0
  141. package/template/src/components/dashboard/contacts-chart.tsx +63 -0
  142. package/template/src/components/dashboard/recent-activity.tsx +164 -0
  143. package/template/src/components/dashboard/sales-analytics-chart.tsx +81 -0
  144. package/template/src/components/dashboard/stat-card.tsx +61 -0
  145. package/template/src/components/dashboard/status-distribution-chart.tsx +45 -0
  146. package/template/src/components/dashboard/tasks-pie-chart.tsx +88 -0
  147. package/template/src/components/dashboard/top-contacts-list.tsx +129 -0
  148. package/template/src/components/dashboard/upcoming-tasks-list.tsx +126 -0
  149. package/template/src/components/editor.tsx +856 -0
  150. package/template/src/components/email-template.tsx +35 -0
  151. package/template/src/components/header.tsx +320 -0
  152. package/template/src/components/invitation-email-template.tsx +79 -0
  153. package/template/src/components/meet-cancellation-email-template.tsx +120 -0
  154. package/template/src/components/meet-confirmation-email-template.tsx +156 -0
  155. package/template/src/components/meet-update-email-template.tsx +209 -0
  156. package/template/src/components/page-header.tsx +61 -0
  157. package/template/src/components/reset-password-email-template.tsx +79 -0
  158. package/template/src/components/sidebar.tsx +294 -0
  159. package/template/src/components/skeleton.tsx +380 -0
  160. package/template/src/components/ui/commands.tsx +396 -0
  161. package/template/src/components/ui/components.tsx +150 -0
  162. package/template/src/components/ui/theme.tsx +5 -0
  163. package/template/src/components/view-as-banner.tsx +45 -0
  164. package/template/src/components/view-as-modal.tsx +186 -0
  165. package/template/src/contexts/mobile-menu-context.tsx +31 -0
  166. package/template/src/contexts/sidebar-context.tsx +107 -0
  167. package/template/src/contexts/task-reminder-context.tsx +239 -0
  168. package/template/src/contexts/view-as-context.tsx +84 -0
  169. package/template/src/hooks/use-user-role.ts +82 -0
  170. package/template/src/lib/audit-log.ts +45 -0
  171. package/template/src/lib/auth-client.ts +16 -0
  172. package/template/src/lib/auth.ts +35 -0
  173. package/template/src/lib/check-permission.ts +193 -0
  174. package/template/src/lib/contact-duplicate.ts +112 -0
  175. package/template/src/lib/contact-interactions.ts +371 -0
  176. package/template/src/lib/encryption.ts +99 -0
  177. package/template/src/lib/google-calendar.ts +300 -0
  178. package/template/src/lib/google-drive.ts +372 -0
  179. package/template/src/lib/permissions.ts +412 -0
  180. package/template/src/lib/prisma.ts +32 -0
  181. package/template/src/lib/roles.ts +120 -0
  182. package/template/src/lib/template-variables.ts +76 -0
  183. package/template/src/lib/utils.ts +46 -0
  184. package/template/src/lib/workflow-executor.ts +482 -0
  185. package/template/src/proxy.ts +91 -0
  186. package/template/tsconfig.json +34 -0
  187. package/template/vercel.json +8 -0
@@ -0,0 +1,200 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { useRouter, useParams } from 'next/navigation';
5
+ import { Eye, EyeOff } from 'lucide-react';
6
+
7
+ export default function InvitePage() {
8
+ const router = useRouter();
9
+ const params = useParams();
10
+ const token = params.token as string;
11
+
12
+ const [password, setPassword] = useState('');
13
+ const [confirmPassword, setConfirmPassword] = useState('');
14
+ const [error, setError] = useState('');
15
+ const [loading, setLoading] = useState(false);
16
+ const [validating, setValidating] = useState(true);
17
+ const [userEmail, setUserEmail] = useState('');
18
+ const [userName, setUserName] = useState('');
19
+ const [showPassword, setShowPassword] = useState(false);
20
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
21
+
22
+ // Vérifier que le token est valide au chargement
23
+ useEffect(() => {
24
+ const validateToken = async () => {
25
+ try {
26
+ const response = await fetch(`/api/invite/validate?token=${token}`);
27
+ const data = await response.json();
28
+
29
+ if (!response.ok) {
30
+ setError(data.error || 'Lien invalide ou expiré');
31
+ setValidating(false);
32
+ return;
33
+ }
34
+
35
+ setUserEmail(data.email);
36
+ setUserName(data.name || '');
37
+ setValidating(false);
38
+ } catch (err) {
39
+ setError('Erreur lors de la validation du lien');
40
+ setValidating(false);
41
+ }
42
+ };
43
+
44
+ if (token) {
45
+ validateToken();
46
+ }
47
+ }, [token]);
48
+
49
+ const handleSubmit = async (e: React.FormEvent) => {
50
+ e.preventDefault();
51
+ setError('');
52
+
53
+ if (password !== confirmPassword) {
54
+ setError('Les mots de passe ne correspondent pas');
55
+ return;
56
+ }
57
+
58
+ if (password.length < 6) {
59
+ setError('Le mot de passe doit contenir au moins 6 caractères');
60
+ return;
61
+ }
62
+
63
+ setLoading(true);
64
+
65
+ try {
66
+ const response = await fetch('/api/invite/complete', {
67
+ method: 'POST',
68
+ headers: { 'Content-Type': 'application/json' },
69
+ body: JSON.stringify({ token, password }),
70
+ });
71
+
72
+ const data = await response.json();
73
+
74
+ if (!response.ok) {
75
+ throw new Error(data.error || 'Erreur lors de la définition du mot de passe');
76
+ }
77
+
78
+ // Rediriger vers la page de connexion
79
+ router.push(
80
+ '/signin?message=Mot de passe défini avec succès, vous pouvez maintenant vous connecter',
81
+ );
82
+ } catch (err: any) {
83
+ setError(err.message);
84
+ } finally {
85
+ setLoading(false);
86
+ }
87
+ };
88
+
89
+ if (validating) {
90
+ return (
91
+ <div className="flex min-h-screen items-center justify-center px-4">
92
+ <div className="w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl">
93
+ <p className="text-gray-600">Vérification du lien...</p>
94
+ </div>
95
+ </div>
96
+ );
97
+ }
98
+
99
+ if (error && !userEmail) {
100
+ return (
101
+ <div className="flex min-h-screen items-center justify-center px-4">
102
+ <div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
103
+ <div className="text-center">
104
+ <h1 className="text-2xl font-bold text-red-600">Lien invalide</h1>
105
+ <p className="mt-4 text-gray-600">{error}</p>
106
+ <button
107
+ onClick={() => router.push('/signin')}
108
+ className="mt-6 cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700"
109
+ >
110
+ Retour à la connexion
111
+ </button>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ );
116
+ }
117
+
118
+ return (
119
+ <div className="flex min-h-screen items-center justify-center px-4">
120
+ <div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl sm:p-8">
121
+ <div className="mb-6 text-center sm:mb-8">
122
+ <h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">
123
+ Définir votre mot de passe
124
+ </h1>
125
+ {userName && (
126
+ <p className="mt-2 text-sm text-gray-600">
127
+ Bienvenue, <span className="font-semibold">{userName}</span>
128
+ </p>
129
+ )}
130
+ <p className="mt-1 text-sm break-all text-gray-500">{userEmail}</p>
131
+ </div>
132
+
133
+ {error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
134
+
135
+ <form onSubmit={handleSubmit} className="space-y-6">
136
+ <div>
137
+ <label className="block text-sm font-medium text-gray-700">Mot de passe</label>
138
+ <div className="relative mt-1">
139
+ <input
140
+ type={showPassword ? 'text' : 'password'}
141
+ required
142
+ minLength={6}
143
+ value={password}
144
+ onChange={(e) => setPassword(e.target.value)}
145
+ className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
146
+ placeholder="••••••••"
147
+ />
148
+ <button
149
+ type="button"
150
+ onClick={() => setShowPassword((prev) => !prev)}
151
+ className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
152
+ aria-label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
153
+ >
154
+ {showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
155
+ </button>
156
+ </div>
157
+ <p className="mt-1 text-xs text-gray-500">Minimum 6 caractères</p>
158
+ </div>
159
+
160
+ <div>
161
+ <label className="block text-sm font-medium text-gray-700">
162
+ Confirmer le mot de passe
163
+ </label>
164
+ <div className="relative mt-1">
165
+ <input
166
+ type={showConfirmPassword ? 'text' : 'password'}
167
+ required
168
+ minLength={6}
169
+ value={confirmPassword}
170
+ onChange={(e) => setConfirmPassword(e.target.value)}
171
+ className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
172
+ placeholder="••••••••"
173
+ />
174
+ <button
175
+ type="button"
176
+ onClick={() => setShowConfirmPassword((prev) => !prev)}
177
+ className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
178
+ aria-label={
179
+ showConfirmPassword
180
+ ? 'Masquer la confirmation du mot de passe'
181
+ : 'Afficher la confirmation du mot de passe'
182
+ }
183
+ >
184
+ {showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
185
+ </button>
186
+ </div>
187
+ </div>
188
+
189
+ <button
190
+ type="submit"
191
+ disabled={loading}
192
+ className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
193
+ >
194
+ {loading ? 'Création du compte...' : 'Créer mon compte'}
195
+ </button>
196
+ </form>
197
+ </div>
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,3 @@
1
+ export default function AuthLayout({ children }: { children: React.ReactNode }) {
2
+ return <div className="bg-lienar-to-br min-h-screen from-blue-50 to-indigo-100">{children}</div>;
3
+ }
@@ -0,0 +1,213 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, Suspense } from 'react';
4
+ import { useRouter, useSearchParams } from 'next/navigation';
5
+ import { Eye, EyeOff } from 'lucide-react';
6
+
7
+ function ResetPasswordCompleteContent() {
8
+ const router = useRouter();
9
+ const searchParams = useSearchParams();
10
+ const token = searchParams.get('token') || '';
11
+
12
+ const [password, setPassword] = useState('');
13
+ const [confirmPassword, setConfirmPassword] = useState('');
14
+ const [error, setError] = useState('');
15
+ const [loading, setLoading] = useState(false);
16
+ const [validating, setValidating] = useState(true);
17
+ const [userEmail, setUserEmail] = useState('');
18
+ const [showPassword, setShowPassword] = useState(false);
19
+ const [showConfirmPassword, setShowConfirmPassword] = useState(false);
20
+
21
+ // Vérifier que le token est valide au chargement
22
+ useEffect(() => {
23
+ const validateToken = async () => {
24
+ try {
25
+ const response = await fetch(`/api/reset-password/validate?token=${token}`);
26
+ const data = await response.json();
27
+
28
+ if (!response.ok) {
29
+ setError(data.error || 'Lien invalide ou expiré');
30
+ setValidating(false);
31
+ return;
32
+ }
33
+
34
+ setUserEmail(data.email);
35
+ setValidating(false);
36
+ } catch (err) {
37
+ setError('Erreur lors de la validation du lien');
38
+ setValidating(false);
39
+ }
40
+ };
41
+
42
+ if (token) {
43
+ validateToken();
44
+ } else {
45
+ setError('Token manquant');
46
+ setValidating(false);
47
+ }
48
+ }, [token]);
49
+
50
+ const handleSubmit = async (e: React.FormEvent) => {
51
+ e.preventDefault();
52
+ setError('');
53
+
54
+ if (password !== confirmPassword) {
55
+ setError('Les mots de passe ne correspondent pas');
56
+ return;
57
+ }
58
+
59
+ if (password.length < 6) {
60
+ setError('Le mot de passe doit contenir au moins 6 caractères');
61
+ return;
62
+ }
63
+
64
+ setLoading(true);
65
+
66
+ try {
67
+ const response = await fetch('/api/reset-password/complete', {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({ token, password }),
71
+ });
72
+
73
+ const data = await response.json();
74
+
75
+ if (!response.ok) {
76
+ throw new Error(data.error || 'Erreur lors de la réinitialisation du mot de passe');
77
+ }
78
+
79
+ // Rediriger vers la page de connexion
80
+ router.push(
81
+ '/signin?message=Mot de passe réinitialisé avec succès, vous pouvez maintenant vous connecter',
82
+ );
83
+ } catch (err: any) {
84
+ setError(err.message);
85
+ } finally {
86
+ setLoading(false);
87
+ }
88
+ };
89
+
90
+ if (validating) {
91
+ return (
92
+ <div className="flex min-h-screen items-center justify-center px-4">
93
+ <div className="w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl">
94
+ <p className="text-gray-600">Vérification du lien...</p>
95
+ </div>
96
+ </div>
97
+ );
98
+ }
99
+
100
+ if (error && !userEmail) {
101
+ return (
102
+ <div className="flex min-h-screen items-center justify-center px-4">
103
+ <div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl">
104
+ <div className="text-center">
105
+ <h1 className="text-2xl font-bold text-red-600">Lien invalide</h1>
106
+ <p className="mt-4 text-gray-600">{error}</p>
107
+ <button
108
+ onClick={() => router.push('/signin')}
109
+ className="mt-6 cursor-pointer rounded-lg bg-indigo-600 px-4 py-2 text-white hover:bg-indigo-700"
110
+ >
111
+ Retour à la connexion
112
+ </button>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ );
117
+ }
118
+
119
+ return (
120
+ <div className="flex min-h-screen items-center justify-center px-4">
121
+ <div className="w-full max-w-md rounded-2xl bg-white p-6 shadow-xl sm:p-8">
122
+ <div className="mb-6 text-center sm:mb-8">
123
+ <h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">Nouveau mot de passe</h1>
124
+ <p className="mt-2 text-sm text-gray-600">
125
+ Définissez un nouveau mot de passe pour votre compte
126
+ </p>
127
+ <p className="mt-1 text-sm break-all text-gray-500">{userEmail}</p>
128
+ </div>
129
+
130
+ {error && <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>}
131
+
132
+ <form onSubmit={handleSubmit} className="space-y-6">
133
+ <div>
134
+ <label className="block text-sm font-medium text-gray-700">Nouveau mot de passe</label>
135
+ <div className="relative mt-1">
136
+ <input
137
+ type={showPassword ? 'text' : 'password'}
138
+ required
139
+ minLength={6}
140
+ value={password}
141
+ onChange={(e) => setPassword(e.target.value)}
142
+ className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
143
+ placeholder="••••••••"
144
+ />
145
+ <button
146
+ type="button"
147
+ onClick={() => setShowPassword((prev) => !prev)}
148
+ className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
149
+ aria-label={showPassword ? 'Masquer le mot de passe' : 'Afficher le mot de passe'}
150
+ >
151
+ {showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
152
+ </button>
153
+ </div>
154
+ <p className="mt-1 text-xs text-gray-500">Minimum 6 caractères</p>
155
+ </div>
156
+
157
+ <div>
158
+ <label className="block text-sm font-medium text-gray-700">
159
+ Confirmer le mot de passe
160
+ </label>
161
+ <div className="relative mt-1">
162
+ <input
163
+ type={showConfirmPassword ? 'text' : 'password'}
164
+ required
165
+ minLength={6}
166
+ value={confirmPassword}
167
+ onChange={(e) => setConfirmPassword(e.target.value)}
168
+ className="block w-full rounded-lg border border-gray-300 px-4 py-3 pr-10 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
169
+ placeholder="••••••••"
170
+ />
171
+ <button
172
+ type="button"
173
+ onClick={() => setShowConfirmPassword((prev) => !prev)}
174
+ className="absolute inset-y-0 right-0 flex cursor-pointer items-center pr-3 text-gray-400 hover:text-gray-600"
175
+ aria-label={
176
+ showConfirmPassword
177
+ ? 'Masquer la confirmation du mot de passe'
178
+ : 'Afficher la confirmation du mot de passe'
179
+ }
180
+ >
181
+ {showConfirmPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
182
+ </button>
183
+ </div>
184
+ </div>
185
+
186
+ <button
187
+ type="submit"
188
+ disabled={loading}
189
+ className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
190
+ >
191
+ {loading ? 'Réinitialisation...' : 'Réinitialiser le mot de passe'}
192
+ </button>
193
+ </form>
194
+ </div>
195
+ </div>
196
+ );
197
+ }
198
+
199
+ export default function ResetPasswordCompletePage() {
200
+ return (
201
+ <Suspense
202
+ fallback={
203
+ <div className="flex min-h-screen items-center justify-center px-4">
204
+ <div className="w-full max-w-md rounded-2xl bg-white p-8 text-center shadow-xl">
205
+ <p className="text-gray-600">Chargement...</p>
206
+ </div>
207
+ </div>
208
+ }
209
+ >
210
+ <ResetPasswordCompleteContent />
211
+ </Suspense>
212
+ );
213
+ }
@@ -0,0 +1,146 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+ import Link from 'next/link';
6
+
7
+ export default function ResetPasswordPage() {
8
+ const router = useRouter();
9
+ const [email, setEmail] = useState('');
10
+ const [error, setError] = useState('');
11
+ const [success, setSuccess] = useState(false);
12
+ const [loading, setLoading] = useState(false);
13
+
14
+ const handleSubmit = async (e: React.FormEvent) => {
15
+ e.preventDefault();
16
+ setError('');
17
+ setLoading(true);
18
+
19
+ try {
20
+ const response = await fetch('/api/reset-password/request', {
21
+ method: 'POST',
22
+ headers: { 'Content-Type': 'application/json' },
23
+ body: JSON.stringify({ email }),
24
+ });
25
+
26
+ const data = await response.json();
27
+
28
+ if (!response.ok) {
29
+ throw new Error(data.error || "Erreur lors de l'envoi du code");
30
+ }
31
+
32
+ setSuccess(true);
33
+ } catch (err: any) {
34
+ setError(err.message || "Erreur lors de l'envoi du code");
35
+ } finally {
36
+ setLoading(false);
37
+ }
38
+ };
39
+
40
+ if (success) {
41
+ return (
42
+ <div className="flex min-h-screen items-center justify-center px-4">
43
+ <div className="w-full max-w-md">
44
+ <div className="rounded-2xl bg-white p-6 shadow-xl sm:p-8">
45
+ <div className="mb-6 text-center sm:mb-8">
46
+ <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-green-100 sm:h-16 sm:w-16">
47
+ <svg
48
+ className="h-6 w-6 text-green-600 sm:h-8 sm:w-8"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ viewBox="0 0 24 24"
52
+ >
53
+ <path
54
+ strokeLinecap="round"
55
+ strokeLinejoin="round"
56
+ strokeWidth={2}
57
+ d="M5 13l4 4L19 7"
58
+ />
59
+ </svg>
60
+ </div>
61
+ <h1 className="text-xl font-bold text-gray-900 sm:text-2xl">Code envoyé</h1>
62
+ <p className="mt-2 text-sm break-all text-gray-600">
63
+ Un code à 6 chiffres a été envoyé à <strong>{email}</strong>
64
+ </p>
65
+ <p className="mt-2 text-sm text-gray-500">
66
+ Vérifiez votre boîte de réception et votre dossier spam.
67
+ </p>
68
+ </div>
69
+
70
+ <button
71
+ onClick={() =>
72
+ router.push(`/reset-password/verify?email=${encodeURIComponent(email)}`)
73
+ }
74
+ className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700"
75
+ >
76
+ Continuer
77
+ </button>
78
+
79
+ <Link
80
+ href="/signin"
81
+ className="mt-4 block text-center text-sm text-indigo-600 hover:text-indigo-700"
82
+ >
83
+ Retour à la connexion
84
+ </Link>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ return (
92
+ <div className="flex min-h-screen items-center justify-center px-4">
93
+ <div className="w-full max-w-md">
94
+ <div className="rounded-2xl bg-white p-6 shadow-xl sm:p-8">
95
+ {/* Header */}
96
+ <div className="mb-6 text-center sm:mb-8">
97
+ <h1 className="text-2xl font-bold text-gray-900 sm:text-3xl">
98
+ Réinitialiser le mot de passe
99
+ </h1>
100
+ <p className="mt-2 text-sm text-gray-600">
101
+ Entrez votre adresse email pour recevoir un code de réinitialisation
102
+ </p>
103
+ </div>
104
+
105
+ {/* Error Message */}
106
+ {error && (
107
+ <div className="mb-4 rounded-lg bg-red-50 p-4 text-sm text-red-600">{error}</div>
108
+ )}
109
+
110
+ {/* Form */}
111
+ <form onSubmit={handleSubmit} className="space-y-6">
112
+ <div>
113
+ <label htmlFor="email" className="block text-sm font-medium text-gray-700">
114
+ Email
115
+ </label>
116
+ <input
117
+ id="email"
118
+ type="email"
119
+ required
120
+ value={email}
121
+ onChange={(e) => setEmail(e.target.value)}
122
+ className="mt-1 block w-full rounded-lg border border-gray-300 px-4 py-3 text-gray-900 placeholder-gray-400 focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500 focus:outline-none"
123
+ placeholder="vous@exemple.com"
124
+ />
125
+ </div>
126
+
127
+ <button
128
+ type="submit"
129
+ disabled={loading}
130
+ className="w-full cursor-pointer rounded-lg bg-indigo-600 px-4 py-3 font-semibold text-white transition-colors hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
131
+ >
132
+ {loading ? 'Envoi...' : 'Envoyer le code'}
133
+ </button>
134
+ </form>
135
+
136
+ <Link
137
+ href="/signin"
138
+ className="mt-4 block text-center text-sm text-indigo-600 hover:text-indigo-700"
139
+ >
140
+ Retour à la connexion
141
+ </Link>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ );
146
+ }