Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 13s
176 lines
9.2 KiB
TypeScript
176 lines
9.2 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { supabase } from '../services/supabaseClient';
|
|
import { LoadingSpinner } from './LoadingSpinner';
|
|
import { XMarkIcon } from './icons/XMarkIcon';
|
|
import { GoogleIcon } from './icons/GoogleIcon';
|
|
import { GithubIcon } from './icons/GithubIcon';
|
|
import { PasswordStrength } from './PasswordStrength';
|
|
import { EyeIcon } from './icons/EyeIcon';
|
|
import { EyeSlashIcon } from './icons/EyeSlashIcon';
|
|
|
|
interface SignUpModalProps {
|
|
isOpen: boolean;
|
|
onClose: () => void;
|
|
onSwitchToSignIn: () => void;
|
|
}
|
|
|
|
export const SignUpModal: React.FC<SignUpModalProps> = ({ isOpen, onClose, onSwitchToSignIn }) => {
|
|
const [email, setEmail] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [confirmPassword, setConfirmPassword] = useState('');
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
setLoading(true);
|
|
|
|
// Add password confirmation check
|
|
if (password !== confirmPassword) {
|
|
setError("Passwords do not match.");
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
setError(null);
|
|
setMessage(null);
|
|
|
|
// Check if there is a current anonymous session
|
|
const { data: { session } } = await supabase.auth.getSession();
|
|
|
|
if (session && session.user.is_anonymous) {
|
|
// If the user is anonymous, upgrade their account instead of creating a new one.
|
|
try {
|
|
const { error } = await supabase.auth.updateUser({ email, password });
|
|
if (error) throw error;
|
|
setMessage('Your account has been created! Check your email for a confirmation link.');
|
|
} catch (err) {
|
|
// We perform a type check to ensure 'err' is an error object before accessing its message property.
|
|
// This provides type safety without using 'any' or 'unknown'.
|
|
const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred during account upgrade.';
|
|
setError(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
} else {
|
|
// Standard sign-up flow for new users.
|
|
try {
|
|
const { error } = await supabase.auth.signUp({
|
|
email,
|
|
password,
|
|
options: { emailRedirectTo: window.location.href }
|
|
});
|
|
if (error) throw error;
|
|
setMessage('Check your email for the confirmation link!');
|
|
} catch (err) {
|
|
// We perform a type check to ensure 'err' is an error object before accessing its message property.
|
|
const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
|
|
setError(errorMessage);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleOAuthSignIn = async (provider: 'google' | 'github') => {
|
|
setLoading(true);
|
|
setError(null);
|
|
const { error } = await supabase.auth.signInWithOAuth({
|
|
provider,
|
|
options: {
|
|
redirectTo: window.location.href,
|
|
}
|
|
});
|
|
if (error) {
|
|
setError(error.message);
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
if (!isOpen) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 bg-black bg-opacity-60 z-50 flex justify-center items-center p-4"
|
|
onClick={onClose}
|
|
aria-modal="true"
|
|
role="dialog"
|
|
>
|
|
<div
|
|
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md relative"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
<button
|
|
onClick={onClose}
|
|
className="absolute top-3 right-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 transition-colors"
|
|
aria-label="Close sign up modal"
|
|
>
|
|
<XMarkIcon className="w-6 h-6" />
|
|
</button>
|
|
|
|
<div className="p-8">
|
|
<h2 className="text-2xl font-bold text-center text-gray-800 dark:text-white mb-2">
|
|
Create an Account
|
|
</h2>
|
|
<p className="text-center text-gray-500 dark:text-gray-400 mb-6 text-sm">
|
|
to start personalizing your experience.
|
|
</p>
|
|
|
|
<div className="space-y-3">
|
|
<button onClick={() => handleOAuthSignIn('google')} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
<GoogleIcon className="w-5 h-5 mr-3" />
|
|
Continue with Google
|
|
</button>
|
|
<button onClick={() => handleOAuthSignIn('github')} className="w-full flex items-center justify-center px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
<GithubIcon className="w-5 h-5 mr-3" />
|
|
Continue with GitHub
|
|
</button>
|
|
</div>
|
|
|
|
<div className="my-6 flex items-center">
|
|
<div className="flex-grow border-t border-gray-300 dark:border-gray-600"></div>
|
|
<span className="flex-shrink mx-4 text-gray-400 text-sm">OR</span>
|
|
<div className="flex-grow border-t border-gray-300 dark:border-gray-600"></div>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div>
|
|
<label htmlFor="email-signup" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Email address</label>
|
|
<input id="email-signup" type="email" value={email} onChange={e => setEmail(e.target.value)} required className="mt-1 block w-full px-3 py-2 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-brand-primary focus:border-brand-primary" placeholder="you@example.com" />
|
|
</div>
|
|
<div>
|
|
<label htmlFor="password-signup" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
|
<div className="relative mt-1">
|
|
<input id="password-signup" type={showPassword ? 'text' : 'password'} value={password} onChange={e => setPassword(e.target.value)} required className="block w-full px-3 py-2 pr-10 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-brand-primary focus:border-brand-primary" placeholder="••••••••" />
|
|
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" aria-label={showPassword ? 'Hide password' : 'Show password'}>
|
|
{showPassword ? <EyeSlashIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
{password.length > 0 && <PasswordStrength password={password} />}
|
|
</div>
|
|
<div>
|
|
<label htmlFor="confirm-password-signup" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirm Password</label>
|
|
<div className="relative mt-1">
|
|
<input id="confirm-password-signup" type={showPassword ? 'text' : 'password'} value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} required className="block w-full px-3 py-2 pr-10 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-brand-primary focus:border-brand-primary" placeholder="••••••••" />
|
|
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 right-0 flex items-center pr-3 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" aria-label={showPassword ? 'Hide password' : 'Show password'}>
|
|
{showPassword ? <EyeSlashIcon className="h-5 w-5" /> : <EyeIcon className="h-5 w-5" />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{error && <p className="text-sm text-red-600 dark:text-red-400 text-center">{error}</p>}
|
|
{message && <p className="text-sm text-green-600 dark:text-green-400 text-center">{message}</p>}
|
|
<button type="submit" disabled={loading || !!message} className="w-full bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 disabled:cursor-not-allowed text-white font-bold py-2.5 px-4 rounded-lg transition-colors duration-300 flex items-center justify-center">
|
|
{loading ? <div className="w-5 h-5"><LoadingSpinner /></div> : 'Create Account'}
|
|
</button>
|
|
<div className="text-center">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Already have an account? <button type="button" onClick={onSwitchToSignIn} className="font-medium text-brand-primary hover:underline">Sign In</button>
|
|
</p>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}; |