Эх сурвалжийг харах

feat(auth): add forgot/reset password flow and work hours validation

- Add ForgotPassword page with email reset link functionality
- Add ResetPassword page to handle password update from email link
- Add "Forgot password?" links to Login and Signup pages
- Add work hours validation to prevent invalid time ranges (closing before opening)
- Auto-adjust closing time when opening time changes
- Add translations for all new features (EN, HU, DE)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 сар өмнө
parent
commit
042b215c14

+ 4 - 0
shopcall.ai-main/src/App.tsx

@@ -16,6 +16,8 @@ import { LoadingScreen } from "@/components/ui/loading-screen";
 import Index from "./pages/Index";
 import Login from "./pages/Login";
 import Signup from "./pages/Signup";
+import ForgotPassword from "./pages/ForgotPassword";
+import ResetPassword from "./pages/ResetPassword";
 
 // Non-critical pages (lazy loaded)
 const Dashboard = lazy(() => import("./pages/Dashboard"));
@@ -61,6 +63,8 @@ const App = () => (
               <Route path="/" element={<Index />} />
               <Route path="/signup" element={<Signup />} />
               <Route path="/login" element={<Login />} />
+              <Route path="/forgot-password" element={<ForgotPassword />} />
+              <Route path="/reset-password" element={<ResetPassword />} />
 
               {/* Lazy-loaded routes */}
               <Route path="/otp" element={<OTP />} />

+ 73 - 7
shopcall.ai-main/src/components/AIConfigContent.tsx

@@ -78,6 +78,23 @@ const TIME_OPTIONS = [
   '20:00', '20:30', '21:00', '21:30', '22:00', '22:30', '23:00', '23:30'
 ];
 
+// Helper to convert time string to minutes for comparison
+const timeToMinutes = (time: string): number => {
+  const [hours, minutes] = time.split(':').map(Number);
+  return hours * 60 + minutes;
+};
+
+// Helper to check if closing time is after opening time
+const isValidTimeRange = (openTime: string, closeTime: string): boolean => {
+  return timeToMinutes(closeTime) > timeToMinutes(openTime);
+};
+
+// Get valid closing time options (only times after opening time)
+const getValidClosingTimes = (openTime: string): string[] => {
+  const openMinutes = timeToMinutes(openTime);
+  return TIME_OPTIONS.filter(time => timeToMinutes(time) > openMinutes);
+};
+
 const DEFAULT_DAILY_HOURS: DailyHours = {
   monday: { is_open: true, open: '09:00', close: '18:00' },
   tuesday: { is_open: true, open: '09:00', close: '18:00' },
@@ -406,6 +423,16 @@ export function AIConfigContent() {
       return;
     }
 
+    // Validate time range if not closed
+    if (!newSpecialIsClosed && !isValidTimeRange(newSpecialOpeningTime, newSpecialClosingTime)) {
+      toast({
+        title: t('common.error'),
+        description: t('aiConfig.businessHours.invalidTimeRange', 'Closing time must be after opening time'),
+        variant: "destructive"
+      });
+      return;
+    }
+
     try {
       const newEntry: Omit<SpecialHours, 'id'> = {
         store_id: selectedShop.id,
@@ -479,6 +506,19 @@ export function AIConfigContent() {
     }
   };
 
+  // Validate all business hours time ranges
+  const validateBusinessHours = (): { isValid: boolean; invalidDay?: string } => {
+    if (!businessHours.is_enabled) return { isValid: true };
+
+    for (const day of WEEKDAYS) {
+      const dayHours = businessHours.daily_hours[day];
+      if (dayHours.is_open && !isValidTimeRange(dayHours.open, dayHours.close)) {
+        return { isValid: false, invalidDay: day };
+      }
+    }
+    return { isValid: true };
+  };
+
   const handleSaveConfig = async () => {
     if (!selectedShop) return;
 
@@ -492,6 +532,20 @@ export function AIConfigContent() {
       return;
     }
 
+    // Validate business hours time ranges
+    const hoursValidation = validateBusinessHours();
+    if (!hoursValidation.isValid) {
+      const dayName = hoursValidation.invalidDay
+        ? t(`aiConfig.businessHours.days.${hoursValidation.invalidDay}`, hoursValidation.invalidDay)
+        : '';
+      toast({
+        title: t('common.error'),
+        description: t('aiConfig.businessHours.invalidTimeRangeDay', { day: dayName }) || `Invalid time range for ${dayName}: closing time must be after opening time`,
+        variant: "destructive"
+      });
+      return;
+    }
+
     // Validate transfer phone number before saving
     const isTransferPhoneValid = await validateTransferPhone(aiConfig.transfer_phone_number);
     if (!isTransferPhoneValid) {
@@ -860,11 +914,16 @@ export function AIConfigContent() {
                                 <Select
                                   value={dayHours.open}
                                   onValueChange={(value) => {
+                                    // Auto-adjust closing time if it would be invalid
+                                    const validClosingTimes = getValidClosingTimes(value);
+                                    const newClose = validClosingTimes.includes(dayHours.close)
+                                      ? dayHours.close
+                                      : validClosingTimes[0] || '23:30';
                                     setBusinessHours({
                                       ...businessHours,
                                       daily_hours: {
                                         ...businessHours.daily_hours,
-                                        [day]: { ...dayHours, open: value }
+                                        [day]: { ...dayHours, open: value, close: newClose }
                                       }
                                     });
                                   }}
@@ -873,13 +932,13 @@ export function AIConfigContent() {
                                     <SelectValue />
                                   </SelectTrigger>
                                   <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
-                                    {TIME_OPTIONS.map((time) => (
+                                    {TIME_OPTIONS.slice(0, -1).map((time) => (
                                       <SelectItem key={time} value={time}>{time}</SelectItem>
                                     ))}
                                   </SelectContent>
                                 </Select>
                                 <span className="text-slate-400">-</span>
-                                {/* Closing Time */}
+                                {/* Closing Time - only show times after opening time */}
                                 <Select
                                   value={dayHours.close}
                                   onValueChange={(value) => {
@@ -896,7 +955,7 @@ export function AIConfigContent() {
                                     <SelectValue />
                                   </SelectTrigger>
                                   <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
-                                    {TIME_OPTIONS.map((time) => (
+                                    {getValidClosingTimes(dayHours.open).map((time) => (
                                       <SelectItem key={time} value={time}>{time}</SelectItem>
                                     ))}
                                   </SelectContent>
@@ -970,13 +1029,20 @@ export function AIConfigContent() {
                           <div className="flex items-center gap-2">
                             <Select
                               value={newSpecialOpeningTime}
-                              onValueChange={setNewSpecialOpeningTime}
+                              onValueChange={(value) => {
+                                setNewSpecialOpeningTime(value);
+                                // Auto-adjust closing time if it would be invalid
+                                const validClosingTimes = getValidClosingTimes(value);
+                                if (!validClosingTimes.includes(newSpecialClosingTime)) {
+                                  setNewSpecialClosingTime(validClosingTimes[0] || '23:30');
+                                }
+                              }}
                             >
                               <SelectTrigger className="bg-slate-700 border-slate-600 text-white w-[85px]">
                                 <SelectValue />
                               </SelectTrigger>
                               <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
-                                {TIME_OPTIONS.map((time) => (
+                                {TIME_OPTIONS.slice(0, -1).map((time) => (
                                   <SelectItem key={time} value={time}>{time}</SelectItem>
                                 ))}
                               </SelectContent>
@@ -990,7 +1056,7 @@ export function AIConfigContent() {
                                 <SelectValue />
                               </SelectTrigger>
                               <SelectContent className="bg-slate-700 border-slate-600 max-h-[200px]">
-                                {TIME_OPTIONS.map((time) => (
+                                {getValidClosingTimes(newSpecialOpeningTime).map((time) => (
                                   <SelectItem key={time} value={time}>{time}</SelectItem>
                                 ))}
                               </SelectContent>

+ 46 - 2
shopcall.ai-main/src/i18n/locales/de.json

@@ -101,7 +101,49 @@
       "submit": "Anmelden",
       "signingIn": "Anmeldung...",
       "noAccount": "Sie haben noch kein Konto?",
-      "signup": "Registrieren"
+      "signup": "Registrieren",
+      "forgotPassword": "Passwort vergessen?"
+    },
+    "forgotPassword": {
+      "title": "Passwort zurücksetzen",
+      "subtitle": "Wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts",
+      "description": "Geben Sie die E-Mail-Adresse ein, die mit Ihrem Konto verknüpft ist, und wir senden Ihnen einen Link zum Zurücksetzen Ihres Passworts.",
+      "email": "E-Mail",
+      "emailPlaceholder": "Geben Sie Ihre E-Mail ein",
+      "submit": "Link zum Zurücksetzen senden",
+      "sending": "Wird gesendet...",
+      "successTitle": "Überprüfen Sie Ihre E-Mail",
+      "successMessage": "Wir haben einen Link zum Zurücksetzen des Passworts an Ihre E-Mail-Adresse gesendet. Bitte überprüfen Sie Ihren Posteingang und folgen Sie den Anweisungen.",
+      "backToLogin": "Zurück zur Anmeldung",
+      "error": "Der Link konnte nicht gesendet werden. Bitte versuchen Sie es erneut."
+    },
+    "resetPassword": {
+      "title": "Neues Passwort festlegen",
+      "subtitle": "Erstellen Sie ein neues Passwort für Ihr Konto",
+      "description": "Bitte geben Sie unten Ihr neues Passwort ein. Stellen Sie sicher, dass es mindestens 8 Zeichen lang ist.",
+      "newPassword": "Neues Passwort",
+      "newPasswordPlaceholder": "Geben Sie Ihr neues Passwort ein",
+      "confirmPassword": "Passwort bestätigen",
+      "confirmPasswordPlaceholder": "Bestätigen Sie Ihr neues Passwort",
+      "passwordRequirement": "Das Passwort muss mindestens 8 Zeichen lang sein",
+      "submit": "Passwort aktualisieren",
+      "updating": "Wird aktualisiert...",
+      "verifying": "Link wird überprüft...",
+      "successTitle": "Passwort aktualisiert",
+      "successMessage": "Ihr Passwort wurde erfolgreich aktualisiert. Sie werden zur Anmeldung weitergeleitet.",
+      "successRedirect": "Ihr Passwort wurde zurückgesetzt. Bitte melden Sie sich mit Ihrem neuen Passwort an.",
+      "redirecting": "Weiterleitung zur Anmeldung...",
+      "backToLogin": "Zurück zur Anmeldung",
+      "invalidLink": {
+        "title": "Ungültiger oder abgelaufener Link",
+        "message": "Dieser Link zum Zurücksetzen des Passworts ist ungültig oder abgelaufen. Bitte fordern Sie einen neuen an.",
+        "requestNew": "Neuen Link anfordern"
+      },
+      "errors": {
+        "passwordMinLength": "Das Passwort muss mindestens 8 Zeichen lang sein",
+        "passwordMismatch": "Die Passwörter stimmen nicht überein",
+        "updateFailed": "Das Passwort konnte nicht aktualisiert werden. Bitte versuchen Sie es erneut."
+      }
     },
     "signup": {
       "title": "Konto Erstellen",
@@ -709,7 +751,9 @@
       "transferPhoneFormat": "Internationales Format erforderlich (z.B. +49301234567)",
       "transferPhoneInvalidFormat": "Ungültiges Format. Verwenden Sie das internationale Format (z.B. +49301234567)",
       "transferPhoneNotAllowed": "Diese Telefonnummer kann nicht als Weiterleitungsnummer verwendet werden",
-      "transferPhoneInvalid": "Ungültige Weiterleitungs-Telefonnummer"
+      "transferPhoneInvalid": "Ungültige Weiterleitungs-Telefonnummer",
+      "invalidTimeRange": "Die Schließzeit muss nach der Öffnungszeit liegen",
+      "invalidTimeRangeDay": "Ungültiger Zeitraum für {{day}}: Die Schließzeit muss nach der Öffnungszeit liegen"
     }
   },
   "onboarding": {

+ 46 - 2
shopcall.ai-main/src/i18n/locales/en.json

@@ -101,7 +101,49 @@
       "submit": "Sign In",
       "signingIn": "Signing In...",
       "noAccount": "Don't have an account?",
-      "signup": "Sign up"
+      "signup": "Sign up",
+      "forgotPassword": "Forgot password?"
+    },
+    "forgotPassword": {
+      "title": "Reset Password",
+      "subtitle": "We'll send you a link to reset your password",
+      "description": "Enter the email address associated with your account and we'll send you a link to reset your password.",
+      "email": "Email",
+      "emailPlaceholder": "Enter your email",
+      "submit": "Send Reset Link",
+      "sending": "Sending...",
+      "successTitle": "Check your email",
+      "successMessage": "We've sent a password reset link to your email address. Please check your inbox and follow the instructions.",
+      "backToLogin": "Back to login",
+      "error": "Failed to send reset link. Please try again."
+    },
+    "resetPassword": {
+      "title": "Set New Password",
+      "subtitle": "Create a new password for your account",
+      "description": "Please enter your new password below. Make sure it's at least 8 characters long.",
+      "newPassword": "New Password",
+      "newPasswordPlaceholder": "Enter your new password",
+      "confirmPassword": "Confirm Password",
+      "confirmPasswordPlaceholder": "Confirm your new password",
+      "passwordRequirement": "Password must be at least 8 characters",
+      "submit": "Update Password",
+      "updating": "Updating...",
+      "verifying": "Verifying your reset link...",
+      "successTitle": "Password Updated",
+      "successMessage": "Your password has been successfully updated. You will be redirected to login.",
+      "successRedirect": "Your password has been reset. Please log in with your new password.",
+      "redirecting": "Redirecting to login...",
+      "backToLogin": "Back to login",
+      "invalidLink": {
+        "title": "Invalid or Expired Link",
+        "message": "This password reset link is invalid or has expired. Please request a new one.",
+        "requestNew": "Request New Reset Link"
+      },
+      "errors": {
+        "passwordMinLength": "Password must be at least 8 characters",
+        "passwordMismatch": "Passwords do not match",
+        "updateFailed": "Failed to update password. Please try again."
+      }
     },
     "signup": {
       "title": "Create Account",
@@ -711,7 +753,9 @@
       "transferPhoneFormat": "International format required (e.g., +36301234567)",
       "transferPhoneInvalidFormat": "Invalid format. Use international format (e.g., +36301234567)",
       "transferPhoneNotAllowed": "This phone number cannot be used as a transfer number",
-      "transferPhoneInvalid": "Invalid transfer phone number"
+      "transferPhoneInvalid": "Invalid transfer phone number",
+      "invalidTimeRange": "Closing time must be after opening time",
+      "invalidTimeRangeDay": "Invalid time range for {{day}}: closing time must be after opening time"
     }
   },
   "onboarding": {

+ 46 - 2
shopcall.ai-main/src/i18n/locales/hu.json

@@ -101,7 +101,49 @@
       "submit": "Bejelentkezés",
       "signingIn": "Bejelentkezés...",
       "noAccount": "Nincs még fiókja?",
-      "signup": "Regisztráció"
+      "signup": "Regisztráció",
+      "forgotPassword": "Elfelejtett jelszó?"
+    },
+    "forgotPassword": {
+      "title": "Jelszó visszaállítása",
+      "subtitle": "Küldünk egy linket a jelszó visszaállításához",
+      "description": "Adja meg a fiókjához tartozó email címet, és küldünk egy linket a jelszó visszaállításához.",
+      "email": "Email",
+      "emailPlaceholder": "Írja be email címét",
+      "submit": "Visszaállító link küldése",
+      "sending": "Küldés...",
+      "successTitle": "Ellenőrizze email fiókját",
+      "successMessage": "Küldtünk egy jelszó-visszaállító linket az email címére. Kérjük, ellenőrizze a bejövő üzeneteit és kövesse az utasításokat.",
+      "backToLogin": "Vissza a bejelentkezéshez",
+      "error": "Nem sikerült elküldeni a visszaállító linket. Kérjük, próbálja újra."
+    },
+    "resetPassword": {
+      "title": "Új jelszó beállítása",
+      "subtitle": "Hozzon létre új jelszót fiókjához",
+      "description": "Kérjük, adja meg új jelszavát alább. Győződjön meg róla, hogy legalább 8 karakter hosszú.",
+      "newPassword": "Új jelszó",
+      "newPasswordPlaceholder": "Írja be új jelszavát",
+      "confirmPassword": "Jelszó megerősítése",
+      "confirmPasswordPlaceholder": "Erősítse meg új jelszavát",
+      "passwordRequirement": "A jelszónak legalább 8 karakter hosszúnak kell lennie",
+      "submit": "Jelszó frissítése",
+      "updating": "Frissítés...",
+      "verifying": "Visszaállító link ellenőrzése...",
+      "successTitle": "Jelszó frissítve",
+      "successMessage": "Jelszava sikeresen frissítve. Átirányítjuk a bejelentkezéshez.",
+      "successRedirect": "Jelszava visszaállításra került. Kérjük, jelentkezzen be új jelszavával.",
+      "redirecting": "Átirányítás a bejelentkezéshez...",
+      "backToLogin": "Vissza a bejelentkezéshez",
+      "invalidLink": {
+        "title": "Érvénytelen vagy lejárt link",
+        "message": "Ez a jelszó-visszaállító link érvénytelen vagy lejárt. Kérjük, kérjen újat.",
+        "requestNew": "Új visszaállító link kérése"
+      },
+      "errors": {
+        "passwordMinLength": "A jelszónak legalább 8 karakter hosszúnak kell lennie",
+        "passwordMismatch": "A jelszavak nem egyeznek",
+        "updateFailed": "Nem sikerült frissíteni a jelszót. Kérjük, próbálja újra."
+      }
     },
     "signup": {
       "title": "Fiók Létrehozása",
@@ -711,7 +753,9 @@
       "transferPhoneFormat": "Nemzetközi formátum szükséges (pl. +36301234567)",
       "transferPhoneInvalidFormat": "Érvénytelen formátum. Használjon nemzetközi formátumot (pl. +36301234567)",
       "transferPhoneNotAllowed": "Ez a telefonszám nem használható átkapcsolási számként",
-      "transferPhoneInvalid": "Érvénytelen átkapcsolási telefonszám"
+      "transferPhoneInvalid": "Érvénytelen átkapcsolási telefonszám",
+      "invalidTimeRange": "A zárási időnek a nyitási idő után kell lennie",
+      "invalidTimeRangeDay": "Érvénytelen időtartomány ({{day}}): a zárási időnek a nyitási idő után kell lennie"
     }
   },
   "onboarding": {

+ 143 - 0
shopcall.ai-main/src/pages/ForgotPassword.tsx

@@ -0,0 +1,143 @@
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Mail, Loader2, CheckCircle, AlertCircle, ArrowLeft } from "lucide-react";
+import { Link } from "react-router-dom";
+import { supabase } from "@/lib/supabase";
+import { useTranslation } from 'react-i18next';
+import { LanguageSelector } from "@/components/LanguageSelector";
+
+const ForgotPassword = () => {
+  const { t } = useTranslation();
+  const [email, setEmail] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState("");
+  const [success, setSuccess] = useState(false);
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setIsLoading(true);
+    setError("");
+
+    try {
+      const { error } = await supabase.auth.resetPasswordForEmail(email, {
+        redirectTo: `${window.location.origin}/reset-password`,
+      });
+
+      if (error) {
+        throw error;
+      }
+
+      setSuccess(true);
+    } catch (error: unknown) {
+      console.error("Password reset failed:", error);
+      setError((error as Error)?.message || t('auth.forgotPassword.error'));
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  return (
+    <div className="min-h-screen bg-slate-900 text-white flex items-center justify-center p-4">
+      <div className="w-full max-w-md">
+        {/* Language Selector */}
+        <div className="flex justify-end mb-4">
+          <LanguageSelector />
+        </div>
+
+        {/* Logo */}
+        <div className="text-center mb-8">
+          <div className="flex items-center justify-center gap-3 mb-4">
+            <img src="/uploads/e0ddbf09-622c-426a-851f-149776e300c0.png" alt="ShopCall.ai" className="w-10 h-10" />
+            <span className="text-2xl font-bold text-white">ShopCall.ai</span>
+          </div>
+          <p className="text-slate-400">{t('auth.forgotPassword.subtitle')}</p>
+        </div>
+
+        <Card className="bg-slate-800 border-slate-700">
+          <CardHeader>
+            <CardTitle className="text-white text-center">{t('auth.forgotPassword.title')}</CardTitle>
+          </CardHeader>
+          <CardContent>
+            {success ? (
+              <div className="space-y-4">
+                <div className="bg-green-900/20 border border-green-500/50 rounded-md p-4 flex items-start gap-3">
+                  <CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
+                  <div>
+                    <p className="text-green-400 font-medium">{t('auth.forgotPassword.successTitle')}</p>
+                    <p className="text-green-400/80 text-sm mt-1">{t('auth.forgotPassword.successMessage')}</p>
+                  </div>
+                </div>
+                <Link to="/login">
+                  <Button
+                    variant="outline"
+                    className="w-full border-slate-600 text-slate-300 hover:bg-slate-700 hover:text-white"
+                  >
+                    <ArrowLeft className="mr-2 h-4 w-4" />
+                    {t('auth.forgotPassword.backToLogin')}
+                  </Button>
+                </Link>
+              </div>
+            ) : (
+              <form onSubmit={handleSubmit} className="space-y-4">
+                <p className="text-slate-400 text-sm text-center mb-4">
+                  {t('auth.forgotPassword.description')}
+                </p>
+
+                {error && (
+                  <div className="bg-red-900/20 border border-red-500/50 rounded-md p-3 flex items-center gap-2">
+                    <AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
+                    <span className="text-red-400 text-sm">{error}</span>
+                  </div>
+                )}
+
+                <div className="space-y-2">
+                  <label className="text-sm font-medium text-slate-300">{t('auth.forgotPassword.email')}</label>
+                  <div className="relative">
+                    <Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                    <Input
+                      type="email"
+                      placeholder={t('auth.forgotPassword.emailPlaceholder')}
+                      value={email}
+                      onChange={(e) => setEmail(e.target.value)}
+                      className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                      required
+                      disabled={isLoading}
+                    />
+                  </div>
+                </div>
+
+                <Button
+                  type="submit"
+                  disabled={isLoading}
+                  className="w-full bg-cyan-500 hover:bg-cyan-600 text-white disabled:opacity-50"
+                >
+                  {isLoading ? (
+                    <>
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      {t('auth.forgotPassword.sending')}
+                    </>
+                  ) : (
+                    t('auth.forgotPassword.submit')
+                  )}
+                </Button>
+              </form>
+            )}
+
+            {!success && (
+              <div className="mt-6 text-center">
+                <Link to="/login" className="text-cyan-400 hover:text-cyan-300 text-sm inline-flex items-center gap-1">
+                  <ArrowLeft className="w-4 h-4" />
+                  {t('auth.forgotPassword.backToLogin')}
+                </Link>
+              </div>
+            )}
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+};
+
+export default ForgotPassword;

+ 6 - 1
shopcall.ai-main/src/pages/Login.tsx

@@ -101,7 +101,12 @@ const Login = () => {
               </div>
 
               <div className="space-y-2">
-                <label className="text-sm font-medium text-slate-300">{t('auth.login.password')}</label>
+                <div className="flex items-center justify-between">
+                  <label className="text-sm font-medium text-slate-300">{t('auth.login.password')}</label>
+                  <a href="/forgot-password" className="text-sm text-cyan-400 hover:text-cyan-300">
+                    {t('auth.login.forgotPassword')}
+                  </a>
+                </div>
                 <div className="relative">
                   <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
                   <Input

+ 279 - 0
shopcall.ai-main/src/pages/ResetPassword.tsx

@@ -0,0 +1,279 @@
+import { useState, useEffect } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Lock, Loader2, CheckCircle, AlertCircle, ArrowLeft } from "lucide-react";
+import { Link, useNavigate } from "react-router-dom";
+import { supabase } from "@/lib/supabase";
+import { useTranslation } from 'react-i18next';
+import { LanguageSelector } from "@/components/LanguageSelector";
+
+const ResetPassword = () => {
+  const { t } = useTranslation();
+  const navigate = useNavigate();
+  const [password, setPassword] = useState("");
+  const [confirmPassword, setConfirmPassword] = useState("");
+  const [isLoading, setIsLoading] = useState(false);
+  const [error, setError] = useState("");
+  const [success, setSuccess] = useState(false);
+  const [isValidSession, setIsValidSession] = useState<boolean | null>(null);
+
+  useEffect(() => {
+    // Check if user has a valid session from the reset link
+    const checkSession = async () => {
+      const { data: { session } } = await supabase.auth.getSession();
+
+      if (session) {
+        setIsValidSession(true);
+      } else {
+        // Listen for auth state changes (handles the case when user clicks the email link)
+        const { data: { subscription } } = supabase.auth.onAuthStateChange(async (event, session) => {
+          if (event === 'PASSWORD_RECOVERY') {
+            setIsValidSession(true);
+          } else if (event === 'SIGNED_IN' && session) {
+            // User might have been redirected with a valid session
+            setIsValidSession(true);
+          }
+        });
+
+        // Set a timeout to show invalid link message if no session is established
+        const timeout = setTimeout(() => {
+          if (isValidSession === null) {
+            setIsValidSession(false);
+          }
+        }, 3000);
+
+        return () => {
+          subscription.unsubscribe();
+          clearTimeout(timeout);
+        };
+      }
+    };
+
+    checkSession();
+  }, []);
+
+  const validatePassword = (password: string): string | null => {
+    if (password.length < 8) {
+      return t('auth.resetPassword.errors.passwordMinLength');
+    }
+    return null;
+  };
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError("");
+
+    // Validate passwords match
+    if (password !== confirmPassword) {
+      setError(t('auth.resetPassword.errors.passwordMismatch'));
+      return;
+    }
+
+    // Validate password strength
+    const passwordError = validatePassword(password);
+    if (passwordError) {
+      setError(passwordError);
+      return;
+    }
+
+    setIsLoading(true);
+
+    try {
+      const { error } = await supabase.auth.updateUser({
+        password: password
+      });
+
+      if (error) {
+        throw error;
+      }
+
+      setSuccess(true);
+
+      // Sign out the user after password reset so they can log in with new password
+      await supabase.auth.signOut();
+
+      // Redirect to login after a short delay
+      setTimeout(() => {
+        navigate('/login', {
+          state: {
+            message: t('auth.resetPassword.successRedirect')
+          }
+        });
+      }, 3000);
+    } catch (error: unknown) {
+      console.error("Password reset failed:", error);
+      setError((error as Error)?.message || t('auth.resetPassword.errors.updateFailed'));
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  // Loading state while checking session
+  if (isValidSession === null) {
+    return (
+      <div className="min-h-screen bg-slate-900 text-white flex items-center justify-center p-4">
+        <div className="text-center">
+          <Loader2 className="w-8 h-8 animate-spin text-cyan-400 mx-auto mb-4" />
+          <p className="text-slate-400">{t('auth.resetPassword.verifying')}</p>
+        </div>
+      </div>
+    );
+  }
+
+  // Invalid or expired link
+  if (isValidSession === false) {
+    return (
+      <div className="min-h-screen bg-slate-900 text-white flex items-center justify-center p-4">
+        <div className="w-full max-w-md">
+          <div className="flex justify-end mb-4">
+            <LanguageSelector />
+          </div>
+
+          <div className="text-center mb-8">
+            <div className="flex items-center justify-center gap-3 mb-4">
+              <img src="/uploads/e0ddbf09-622c-426a-851f-149776e300c0.png" alt="ShopCall.ai" className="w-10 h-10" />
+              <span className="text-2xl font-bold text-white">ShopCall.ai</span>
+            </div>
+          </div>
+
+          <Card className="bg-slate-800 border-slate-700">
+            <CardContent className="pt-6">
+              <div className="text-center space-y-4">
+                <div className="bg-red-900/20 border border-red-500/50 rounded-md p-4 flex items-start gap-3">
+                  <AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
+                  <div className="text-left">
+                    <p className="text-red-400 font-medium">{t('auth.resetPassword.invalidLink.title')}</p>
+                    <p className="text-red-400/80 text-sm mt-1">{t('auth.resetPassword.invalidLink.message')}</p>
+                  </div>
+                </div>
+                <div className="flex flex-col gap-2">
+                  <Link to="/forgot-password">
+                    <Button className="w-full bg-cyan-500 hover:bg-cyan-600 text-white">
+                      {t('auth.resetPassword.invalidLink.requestNew')}
+                    </Button>
+                  </Link>
+                  <Link to="/login">
+                    <Button
+                      variant="outline"
+                      className="w-full border-slate-600 text-slate-300 hover:bg-slate-700 hover:text-white"
+                    >
+                      <ArrowLeft className="mr-2 h-4 w-4" />
+                      {t('auth.resetPassword.backToLogin')}
+                    </Button>
+                  </Link>
+                </div>
+              </div>
+            </CardContent>
+          </Card>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <div className="min-h-screen bg-slate-900 text-white flex items-center justify-center p-4">
+      <div className="w-full max-w-md">
+        {/* Language Selector */}
+        <div className="flex justify-end mb-4">
+          <LanguageSelector />
+        </div>
+
+        {/* Logo */}
+        <div className="text-center mb-8">
+          <div className="flex items-center justify-center gap-3 mb-4">
+            <img src="/uploads/e0ddbf09-622c-426a-851f-149776e300c0.png" alt="ShopCall.ai" className="w-10 h-10" />
+            <span className="text-2xl font-bold text-white">ShopCall.ai</span>
+          </div>
+          <p className="text-slate-400">{t('auth.resetPassword.subtitle')}</p>
+        </div>
+
+        <Card className="bg-slate-800 border-slate-700">
+          <CardHeader>
+            <CardTitle className="text-white text-center">{t('auth.resetPassword.title')}</CardTitle>
+          </CardHeader>
+          <CardContent>
+            {success ? (
+              <div className="space-y-4">
+                <div className="bg-green-900/20 border border-green-500/50 rounded-md p-4 flex items-start gap-3">
+                  <CheckCircle className="w-5 h-5 text-green-400 flex-shrink-0 mt-0.5" />
+                  <div>
+                    <p className="text-green-400 font-medium">{t('auth.resetPassword.successTitle')}</p>
+                    <p className="text-green-400/80 text-sm mt-1">{t('auth.resetPassword.successMessage')}</p>
+                  </div>
+                </div>
+                <div className="flex items-center justify-center gap-2 text-slate-400 text-sm">
+                  <Loader2 className="w-4 h-4 animate-spin" />
+                  {t('auth.resetPassword.redirecting')}
+                </div>
+              </div>
+            ) : (
+              <form onSubmit={handleSubmit} className="space-y-4">
+                <p className="text-slate-400 text-sm text-center mb-4">
+                  {t('auth.resetPassword.description')}
+                </p>
+
+                {error && (
+                  <div className="bg-red-900/20 border border-red-500/50 rounded-md p-3 flex items-center gap-2">
+                    <AlertCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
+                    <span className="text-red-400 text-sm">{error}</span>
+                  </div>
+                )}
+
+                <div className="space-y-2">
+                  <label className="text-sm font-medium text-slate-300">{t('auth.resetPassword.newPassword')}</label>
+                  <div className="relative">
+                    <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                    <Input
+                      type="password"
+                      placeholder={t('auth.resetPassword.newPasswordPlaceholder')}
+                      value={password}
+                      onChange={(e) => setPassword(e.target.value)}
+                      className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                      required
+                      disabled={isLoading}
+                    />
+                  </div>
+                  <p className="text-xs text-slate-400">{t('auth.resetPassword.passwordRequirement')}</p>
+                </div>
+
+                <div className="space-y-2">
+                  <label className="text-sm font-medium text-slate-300">{t('auth.resetPassword.confirmPassword')}</label>
+                  <div className="relative">
+                    <Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400 w-4 h-4" />
+                    <Input
+                      type="password"
+                      placeholder={t('auth.resetPassword.confirmPasswordPlaceholder')}
+                      value={confirmPassword}
+                      onChange={(e) => setConfirmPassword(e.target.value)}
+                      className="pl-10 bg-slate-700 border-slate-600 text-white placeholder-slate-400"
+                      required
+                      disabled={isLoading}
+                    />
+                  </div>
+                </div>
+
+                <Button
+                  type="submit"
+                  disabled={isLoading}
+                  className="w-full bg-cyan-500 hover:bg-cyan-600 text-white disabled:opacity-50"
+                >
+                  {isLoading ? (
+                    <>
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      {t('auth.resetPassword.updating')}
+                    </>
+                  ) : (
+                    t('auth.resetPassword.submit')
+                  )}
+                </Button>
+              </form>
+            )}
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+};
+
+export default ResetPassword;

+ 6 - 1
shopcall.ai-main/src/pages/Signup.tsx

@@ -288,13 +288,18 @@ const Signup = () => {
               </Button>
             </form>
 
-            <div className="mt-6 text-center">
+            <div className="mt-6 text-center space-y-2">
               <p className="text-slate-400 text-sm">
                 {t('signup.alreadyHaveAccount')}{" "}
                 <a href="/login" className="text-cyan-400 hover:text-cyan-300">
                   {t('signup.signIn')}
                 </a>
               </p>
+              <p className="text-slate-400 text-sm">
+                <a href="/forgot-password" className="text-cyan-400 hover:text-cyan-300">
+                  {t('auth.login.forgotPassword')}
+                </a>
+              </p>
             </div>
           </CardContent>
         </Card>