From e0330d4f28af2b1661dfd62514e59551d241a7eb Mon Sep 17 00:00:00 2001 From: soufiane Date: Tue, 2 Dec 2025 16:15:30 +0100 Subject: [PATCH] feat: add reset-password page and update contest dates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add reset-password page to handle password reset flow - Fix forgot-password to call real API - Update contest dates (validation: Dec 1-31, recovery: Dec 1 - Jan 31) - Update draw date to Feb 1, 2026 - Improve GamePeriod and GrandPrize components design - Remove "livré chez vous" text 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- app/forgot-password/page.tsx | 55 ++++++-- app/lots/page.tsx | 2 +- app/page.tsx | 3 +- app/reset-password/page.tsx | 258 +++++++++++++++++++++++++++++++++++ app/rules/page.tsx | 8 +- components/GamePeriod.tsx | 50 ++++--- components/GrandPrize.tsx | 91 ++++++------ 7 files changed, 374 insertions(+), 93 deletions(-) create mode 100644 app/reset-password/page.tsx diff --git a/app/forgot-password/page.tsx b/app/forgot-password/page.tsx index 5c1026a..edec73e 100644 --- a/app/forgot-password/page.tsx +++ b/app/forgot-password/page.tsx @@ -1,23 +1,41 @@ "use client"; import { useState } from "react"; import Link from "next/link"; -import { ROUTES } from "@/utils/constants"; +import { ROUTES, API_BASE_URL } from "@/utils/constants"; export default function ForgotPasswordPage() { const [email, setEmail] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [isSuccess, setIsSuccess] = useState(false); + const [error, setError] = useState(""); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); + setError(""); - // Simulation d'envoi - await new Promise(resolve => setTimeout(resolve, 1500)); + try { + const response = await fetch(`${API_BASE_URL}/auth/forgot-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ email }), + }); - console.log('Password reset email sent to:', email); - setIsSuccess(true); - setIsSubmitting(false); + const data = await response.json(); + + if (data.success) { + setIsSuccess(true); + } else { + setError(data.message || 'Une erreur est survenue'); + } + } catch (err) { + setError('Erreur de connexion au serveur'); + console.error('Forgot password error:', err); + } finally { + setIsSubmitting(false); + } }; if (isSuccess) { @@ -27,9 +45,9 @@ export default function ForgotPasswordPage() { {/* Title */}
-

Email envoyé !

+

Email envoyé !

- VĂ©rifiez votre boĂ®te de rĂ©ception + Vérifiez votre boîte de réception

@@ -43,16 +61,16 @@ export default function ForgotPasswordPage() {

- Lien de rĂ©initialisation envoyĂ© + Lien de réinitialisation envoyé

- Nous avons envoyĂ© un lien de rĂ©initialisation Ă  {email} + Si cet email existe dans notre base, vous recevrez un lien de réinitialisation à {email}

- đź’ˇ Conseil : Si vous ne recevez pas l'email dans quelques minutes, vĂ©rifiez votre dossier spam. + Conseil : Si vous ne recevez pas l'email dans quelques minutes, vérifiez votre dossier spam.

@@ -60,7 +78,7 @@ export default function ForgotPasswordPage() { href={ROUTES.LOGIN} className="inline-flex items-center justify-center w-full bg-[#1a4d2e] hover:bg-[#2d5a3d] text-white font-bold px-8 py-4 rounded-lg transition-all" > - Retour Ă  la connexion + Retour à la connexion @@ -75,9 +93,9 @@ export default function ForgotPasswordPage() { {/* Title */}
-

Mot de passe oublié

+

Mot de passe oublié

- Entrez votre email pour recevoir un lien de rĂ©initialisation + Entrez votre email pour recevoir un lien de réinitialisation

@@ -87,6 +105,13 @@ export default function ForgotPasswordPage() { {/* Form Container */}
+ {/* Error Message */} + {error && ( +
+ {error} +
+ )} + {/* Form */}
@@ -112,7 +137,7 @@ export default function ForgotPasswordPage() { disabled={isSubmitting} className="w-full bg-[#1a4d2e] hover:bg-[#2d5a3d] disabled:bg-gray-400 text-white font-bold px-8 py-4 rounded-lg transition-all" > - {isSubmitting ? "Envoi en cours..." : "Envoyer le lien de réinitialisation"} + {isSubmitting ? "Envoi en cours..." : "Envoyer le lien de r\u00e9initialisation"} {/* Back to Login */} diff --git a/app/lots/page.tsx b/app/lots/page.tsx index b6a201d..4770e93 100644 --- a/app/lots/page.tsx +++ b/app/lots/page.tsx @@ -60,7 +60,7 @@ export default function LotsPage() { 1 an de thé offert

- Le grand prix du tirage final : une année complète de thé premium livré chez vous + Le grand prix du tirage final : une année complète de thé premium

Tirage sous contrĂ´le d'huissier diff --git a/app/page.tsx b/app/page.tsx index 12f8c3e..1d2a0d5 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -69,8 +69,7 @@ export default function HomePage() { {/* Grand Prize Banner */}
diff --git a/app/reset-password/page.tsx b/app/reset-password/page.tsx new file mode 100644 index 0000000..4fc8492 --- /dev/null +++ b/app/reset-password/page.tsx @@ -0,0 +1,258 @@ +"use client"; +import { useState, useEffect, Suspense } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import Link from "next/link"; +import { ROUTES, API_BASE_URL } from "@/utils/constants"; + +function ResetPasswordForm() { + const searchParams = useSearchParams(); + const router = useRouter(); + const token = searchParams.get("token"); + + const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isSuccess, setIsSuccess] = useState(false); + const [error, setError] = useState(""); + const [showPassword, setShowPassword] = useState(false); + + useEffect(() => { + if (!token) { + setError("Token de réinitialisation manquant. Veuillez demander un nouveau lien."); + } + }, [token]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); + + // Validation + if (password.length < 8) { + setError("Le mot de passe doit contenir au moins 8 caractères"); + return; + } + + if (password !== confirmPassword) { + setError("Les mots de passe ne correspondent pas"); + return; + } + + setIsSubmitting(true); + + try { + const response = await fetch(`${API_BASE_URL}/auth/reset-password`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ token, password }), + }); + + const data = await response.json(); + + if (data.success) { + setIsSuccess(true); + // Redirection après 3 secondes + setTimeout(() => { + router.push(ROUTES.LOGIN); + }, 3000); + } else { + setError(data.message || 'Une erreur est survenue'); + } + } catch (err) { + setError('Erreur de connexion au serveur'); + console.error('Reset password error:', err); + } finally { + setIsSubmitting(false); + } + }; + + if (isSuccess) { + return ( +
+
+
+

Mot de passe modifié !

+
+ +
+
+
+ + + +
+ +

+ Votre mot de passe a été réinitialisé +

+ +

+ Vous allez être redirigé vers la page de connexion... +

+ + + Se connecter maintenant + +
+
+
+
+ ); + } + + return ( +
+
+ + {/* Title */} +
+

Nouveau mot de passe

+

+ Choisissez un nouveau mot de passe sécurisé +

+
+ + {/* Main Card */} +
+
+ + {/* Error Message */} + {error && ( +
+ {error} +
+ )} + + {/* Form */} + + + {/* New Password */} +
+ +
+ setPassword(e.target.value)} + placeholder="Minimum 8 caractères" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1a4d2e] focus:border-transparent pr-12" + disabled={!token} + /> + +
+
+ + {/* Confirm Password */} +
+ + setConfirmPassword(e.target.value)} + placeholder="Répétez le mot de passe" + className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#1a4d2e] focus:border-transparent" + disabled={!token} + /> +
+ + {/* Password Requirements */} +
+

Le mot de passe doit contenir :

+
    +
  • = 8 ? 'text-green-600' : ''}`}> + {password.length >= 8 ? ( + + + + ) : ( + + + + )} + Au moins 8 caractères +
  • +
  • 0 ? 'text-green-600' : ''}`}> + {password === confirmPassword && password.length > 0 ? ( + + + + ) : ( + + + + )} + Les mots de passe correspondent +
  • +
+
+ + {/* Submit Button */} + + + {/* Back to Login */} +
+ + Retour Ă  la connexion + +
+ + +
+
+
+
+ ); +} + +export default function ResetPasswordPage() { + return ( + +
+
+

Chargement...

+
+ + }> + +
+ ); +} diff --git a/app/rules/page.tsx b/app/rules/page.tsx index e9713e9..0896c12 100644 --- a/app/rules/page.tsx +++ b/app/rules/page.tsx @@ -47,12 +47,12 @@ export default function RulesPage() {

- {/* 30 + 30 jours */} + {/* 30 + 60 jours */}
🔄
-
30 + 30 jours
+
30 + 60 jours

- Période de jeu + délai de réclamation + Validation tickets (30j) + récupération lots (60j)

@@ -240,7 +240,7 @@ export default function RulesPage() { {openSection === 5 && (
-

Les lots doivent être réclamés dans un délai de 30 jours à compter de la date de participation.

+

Les lots doivent être réclamés dans un délai de 60 jours (du 1er décembre 2025 au 31 janvier 2026).

Modalités de remise :

    diff --git a/components/GamePeriod.tsx b/components/GamePeriod.tsx index 78b82c2..b9191ee 100644 --- a/components/GamePeriod.tsx +++ b/components/GamePeriod.tsx @@ -2,37 +2,45 @@ export default function GamePeriod() { return ( -
    - {/* Période d'achat */} -
    -
    -
    - - +
    + {/* Période de validation des tickets */} +
    +
    +
    + +
    -
    -

    Période d'achat

    -

    - Achetez vos tickets de 49€ en boutique et obtenez votre code de participation +

    Période de validation des tickets

    +

    + Achetez et validez votre code de participation +

    +
    +

    + Du 1 décembre 2025 au 31 décembre 2025

    +

    30 jours

    - {/* Période de jeu */} -
    -
    -
    - - + {/* Période de récupération des lots */} +
    +
    +
    + +
    -
    -

    Période de jeu

    -

    - Utilisez vos tickets pour jouer et découvrir vos lots instantanément +

    Période de récupération des lots

    +

    + Récupérez vos lots gagnés en boutique +

    +
    +

    + Du 1 décembre 2025 au 31 janvier 2026

    +

    60 jours

    diff --git a/components/GrandPrize.tsx b/components/GrandPrize.tsx index 9c7cf89..3b27e81 100644 --- a/components/GrandPrize.tsx +++ b/components/GrandPrize.tsx @@ -3,13 +3,11 @@ interface GrandPrizeProps { prizeAmount?: string; drawDate?: Date; - participantsCount?: number; } export default function GrandPrize({ prizeAmount = "360€", drawDate, - participantsCount, }: GrandPrizeProps) { const formatDate = (date: Date) => { return date.toLocaleDateString('fr-FR', { @@ -20,68 +18,61 @@ export default function GrandPrize({ }; return ( -
    -
    +
    +
    {/* Icône trophée */} -
    -
    - - - -
    +
    + + +
    - {/* Contenu */} -
    -
    -

    - 🎉 Grand Prix à gagner ! -

    -
    + {/* Titre */} +

    + Grand Prix Ă  gagner ! +

    -

    - À la fin du concours, un grand gagnant sera tiré au sort parmi les participants et remportera un{' '} - lot d'une valeur de {prizeAmount} ! + {/* Prix principal */} +

    +

    + 1 an de thé offert

    +

    + d'une valeur de {prizeAmount} +

    +
    -
    - {drawDate && ( -
    - + {/* Description */} +

    + Le grand prix du tirage final : une année complète de thé premium +

    + + {/* Informations du tirage */} +
    + {drawDate && ( +
    +
    + - - Tirage au sort : {formatDate(drawDate)} -
    - )} - - {participantsCount && ( -
    - - - - - {participantsCount.toLocaleString('fr-FR')} participants déjà inscrits - +
    +

    Tirage au sort

    +

    {formatDate(drawDate)}

    - )} +
    + )} -
    - +
    +
    + - - Tirage certifié par un huissier de justice -
    -
    - -
    - - - - Plus vous jouez, plus vous avez de chances de gagner ! +
    +

    Certification

    +

    ContrĂ´le d'huissier

    +