|
|
@@ -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;
|