typescript fixin
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 13s
Some checks failed
Deploy to Web Server flyer-crawler.projectium.com / deploy (push) Failing after 13s
This commit is contained in:
@@ -3,6 +3,8 @@ import { supabase } from '../services/supabaseClient';
|
||||
import { LoadingSpinner } from './LoadingSpinner';
|
||||
import { XMarkIcon } from './icons/XMarkIcon';
|
||||
import { GoogleIcon } from './icons/GoogleIcon';
|
||||
import { EyeIcon } from './icons/EyeIcon';
|
||||
import { EyeSlashIcon } from './icons/EyeSlashIcon';
|
||||
|
||||
interface AuthModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -18,6 +20,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({ isOpen, onClose, onSwitchT
|
||||
const [password, setPassword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
const clearState = () => {
|
||||
@@ -25,6 +28,7 @@ export const AuthModal: React.FC<AuthModalProps> = ({ isOpen, onClose, onSwitchT
|
||||
setMessage(null);
|
||||
setEmail('');
|
||||
setPassword('');
|
||||
setShowPassword(false);
|
||||
}
|
||||
|
||||
const handleViewChange = (newView: AuthView) => {
|
||||
@@ -195,7 +199,12 @@ export const AuthModal: React.FC<AuthModalProps> = ({ isOpen, onClose, onSwitchT
|
||||
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
||||
<button type="button" onClick={() => handleViewChange('resetPassword')} className="text-sm text-brand-primary hover:underline">Forgot password?</button>
|
||||
</div>
|
||||
<input id="password" type="password" value={password} onChange={e => setPassword(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="••••••••" />
|
||||
<div className="relative mt-1">
|
||||
<input id="password" 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>
|
||||
</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>}
|
||||
|
||||
@@ -69,4 +69,42 @@ describe('LoginPage Component', () => {
|
||||
// The error container shouldn't even be in the DOM
|
||||
expect(screen.queryByText(/invalid/i)).toBeNull();
|
||||
});
|
||||
|
||||
it('should not call onLogin if email is empty', async () => {
|
||||
const handleLogin = vi.fn();
|
||||
render(<LoginPage onLogin={handleLogin} error={null} />);
|
||||
|
||||
const emailInput = screen.getByLabelText('Email address');
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign in' });
|
||||
|
||||
// Clear the email field to make it invalid
|
||||
fireEvent.change(emailInput, { target: { value: '' } });
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
// Because of form validation, the submit handler should not be called.
|
||||
// We wait briefly to ensure no async operations are triggered.
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
expect(handleLogin).not.toHaveBeenCalled();
|
||||
// The button should not be in a loading state
|
||||
expect(submitButton).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('should re-enable the submit button after the login attempt is complete', async () => {
|
||||
// This test simulates the full lifecycle of a login attempt.
|
||||
const handleLogin = vi.fn();
|
||||
render(<LoginPage onLogin={handleLogin} error={null} />);
|
||||
const submitButton = screen.getByRole('button', { name: 'Sign in' });
|
||||
|
||||
// Initial state: button is enabled
|
||||
expect(submitButton).toBeEnabled();
|
||||
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
// During submission, button is disabled
|
||||
expect(submitButton).toBeDisabled();
|
||||
|
||||
// After the simulated delay in handleSubmit, the button should be re-enabled
|
||||
await waitFor(() => expect(submitButton).toBeEnabled(), { timeout: 1000 });
|
||||
});
|
||||
});
|
||||
@@ -2,15 +2,19 @@ import React, { useState } from 'react';
|
||||
import { ShoppingCartIcon } from './icons/ShoppingCartIcon';
|
||||
import { LoadingSpinner } from './LoadingSpinner';
|
||||
|
||||
import { EyeIcon } from './icons/EyeIcon';
|
||||
import { EyeSlashIcon } from './icons/EyeSlashIcon';
|
||||
interface LoginPageProps {
|
||||
onLogin: (email: string, pass: string) => void;
|
||||
onClearError: () => void;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export const LoginPage: React.FC<LoginPageProps> = ({ onLogin, error }) => {
|
||||
const [email, setEmail] = useState('test@test.com');
|
||||
export const LoginPage: React.FC<LoginPageProps> = ({ onLogin, onClearError, error }) => {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('pass123');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
@@ -22,6 +26,18 @@ export const LoginPage: React.FC<LoginPageProps> = ({ onLogin, error }) => {
|
||||
}, 500);
|
||||
};
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// When the user starts typing, clear any previous login errors.
|
||||
onClearError();
|
||||
setEmail(e.target.value);
|
||||
};
|
||||
|
||||
const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
// When the user starts typing, clear any previous login errors.
|
||||
onClearError();
|
||||
setPassword(e.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col justify-center items-center bg-gray-100 dark:bg-gray-950 px-6 py-12 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-sm">
|
||||
@@ -50,7 +66,7 @@ export const LoginPage: React.FC<LoginPageProps> = ({ onLogin, error }) => {
|
||||
autoComplete="email"
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
onChange={handleEmailChange}
|
||||
className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 dark:text-white bg-white dark:bg-gray-800 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-brand-secondary sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</div>
|
||||
@@ -62,17 +78,20 @@ export const LoginPage: React.FC<LoginPageProps> = ({ onLogin, error }) => {
|
||||
Password
|
||||
</label>
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
<div className="mt-2 relative">
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
autoComplete="current-password"
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="block w-full rounded-md border-0 py-1.5 px-2 text-gray-900 dark:text-white bg-white dark:bg-gray-800 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-brand-secondary sm:text-sm sm:leading-6"
|
||||
onChange={handlePasswordChange}
|
||||
className="block w-full rounded-md border-0 py-1.5 px-2 pr-10 text-gray-900 dark:text-white bg-white dark:bg-gray-800 shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-gray-700 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-brand-secondary sm:text-sm sm:leading-6"
|
||||
/>
|
||||
<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>
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ import { XMarkIcon } from './icons/XMarkIcon';
|
||||
import { GoogleIcon } from './icons/GoogleIcon';
|
||||
import { GithubIcon } from './icons/GithubIcon';
|
||||
import { ConfirmationModal } from './ConfirmationModal';
|
||||
import { EyeIcon } from './icons/EyeIcon';
|
||||
import { EyeSlashIcon } from './icons/EyeSlashIcon';
|
||||
|
||||
type AuthStatus = 'SIGNED_OUT' | 'ANONYMOUS' | 'AUTHENTICATED';
|
||||
interface ProfileManagerProps {
|
||||
@@ -35,6 +37,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
const [passwordMessage, setPasswordMessage] = useState('');
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
|
||||
// Data & Privacy state
|
||||
const [exportLoading, setExportLoading] = useState(false);
|
||||
@@ -56,6 +59,7 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
setDeleteError('');
|
||||
setPasswordError('');
|
||||
setPasswordMessage('');
|
||||
setShowPassword(false);
|
||||
}
|
||||
}, [isOpen]); // Only depend on isOpen
|
||||
|
||||
@@ -253,11 +257,21 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
<form onSubmit={handlePasswordUpdate} className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">New Password</label>
|
||||
<input id="newPassword" type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="••••••••" 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" />
|
||||
<div className="relative mt-1">
|
||||
<input id="newPassword" type={showPassword ? 'text' : 'password'} value={password} onChange={e => setPassword(e.target.value)} placeholder="••••••••" 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" />
|
||||
<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>
|
||||
<div>
|
||||
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Confirm New Password</label>
|
||||
<input id="confirmPassword" type="password" value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} placeholder="••••••••" 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" />
|
||||
<div className="relative mt-1">
|
||||
<input id="confirmPassword" type={showPassword ? 'text' : 'password'} value={confirmPassword} onChange={e => setConfirmPassword(e.target.value)} placeholder="••••••••" 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" />
|
||||
<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>
|
||||
<div className="pt-2">
|
||||
<button type="submit" disabled={passwordLoading} className="w-full bg-brand-secondary hover:bg-brand-dark disabled:bg-gray-400 text-white font-bold py-2.5 px-4 rounded-lg flex justify-center">
|
||||
@@ -309,15 +323,20 @@ export const ProfileManager: React.FC<ProfileManagerProps> = ({ isOpen, onClose,
|
||||
<p className="text-sm font-medium text-gray-800 dark:text-white">To confirm, please enter your current password.</p>
|
||||
<div>
|
||||
<label htmlFor="delete-password" className="sr-only">Current Password</label>
|
||||
<input
|
||||
id="delete-password"
|
||||
type="password"
|
||||
value={passwordForDelete}
|
||||
onChange={e => setPasswordForDelete(e.target.value)}
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
className="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"
|
||||
/>
|
||||
<div className="relative">
|
||||
<input
|
||||
id="delete-password"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
value={passwordForDelete}
|
||||
onChange={e => setPasswordForDelete(e.target.value)}
|
||||
required
|
||||
placeholder="Enter your password"
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
{deleteError && <p className="text-xs text-red-600 dark:text-red-400 whitespace-pre-wrap font-mono">{deleteError}</p>}
|
||||
<div className="flex flex-col sm:flex-row sm:space-x-2 space-y-2 sm:space-y-0">
|
||||
|
||||
@@ -5,6 +5,8 @@ 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;
|
||||
@@ -19,6 +21,7 @@ export const SignUpModal: React.FC<SignUpModalProps> = ({ isOpen, onClose, onSwi
|
||||
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();
|
||||
@@ -138,12 +141,22 @@ export const SignUpModal: React.FC<SignUpModalProps> = ({ isOpen, onClose, onSwi
|
||||
</div>
|
||||
<div>
|
||||
<label htmlFor="password-signup" className="block text-sm font-medium text-gray-700 dark:text-gray-300">Password</label>
|
||||
<input id="password-signup" type="password" value={password} onChange={e => setPassword(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="••••••••" />
|
||||
<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>
|
||||
<input id="confirm-password-signup" type="password" value={confirmPassword} onChange={e => setConfirmPassword(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="••••••••" />
|
||||
<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>}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
|
||||
export const EyeIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639l4.443-7.532A1.012 1.012 0 0 1 7.23 4.001h9.54a1.012 1.012 0 0 1 .75.311l4.443 7.532a1.012 1.012 0 0 1 0 .639l-4.443 7.531a1.012 1.012 0 0 1-.75.311h-9.54a1.012 1.012 0 0 1-.75-.311L2.036 12.322Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639l4.443-5.332a1.012 1.012 0 011.536 0l4.443 5.332a1.012 1.012 0 010 .639l-4.443 5.332a1.012 1.012 0 01-1.536 0l-4.443-5.332z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
);
|
||||
8
components/icons/EyeSlashIcon.tsx
Normal file
8
components/icons/EyeSlashIcon.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
|
||||
export const EyeSlashIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" {...props}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 001.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0112 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 01-4.293 5.524M2.036 12.322a1.012 1.012 0 010-.639l4.443-5.332a1.012 1.012 0 011.536 0l4.443 5.332a1.012 1.012 0 010 .639l-4.443 5.332a1.012 1.012 0 01-1.536 0l-4.443-5.332z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
);
|
||||
Reference in New Issue
Block a user