L8: Add SignInForm to SignInPage
Integrate the form into the sign-in page
Let's integrate our beautiful form into the sign-in page! 🎨
Remove Placeholder
Replace the placeholder with the actual form:
import SignInForm from '@/components/SignInForm'; // ← New import
function SignInPage() {
return (
<div className="sign-in-page">
<div className="sign-in-container">
{/* Logo/Brand */}
<div className="sign-in-header">
<h1 className="brand-logo">🏖️ Holidaze</h1>
<p className="brand-tagline">Your next adventure awaits</p>
</div>
{/* Sign-in form */}
<div className="sign-in-form-container">
<h2 className="sign-in-title">Sign in to your account</h2>
<p className="sign-in-subtitle">
Welcome back! Please enter your details.
</p>
<SignInForm /> {/* ← Replaced placeholder */}
</div>
{/* Footer */}
<div className="sign-in-footer">
<p className="footer-text">
Don't have an account?{' '}
<a href="/register" className="footer-link">
Sign up
</a>
</p>
</div>
</div>
</div>
);
}
export default SignInPage;Update Styling
Remove placeholder styles and adjust spacing:
/* Remove this (was temporary): */
/* .form-placeholder { ... } */
/* Adjust form container spacing */
.sign-in-form-container {
margin-bottom: 2rem;
}
.sign-in-title {
font-size: 1.5rem;
font-weight: 600;
color: #1f2937;
margin: 0 0 0.5rem 0;
text-align: center;
}
.sign-in-subtitle {
color: #6b7280;
font-size: 0.875rem;
text-align: center;
margin: 0 0 2rem 0; /* Space before form */
}Test Complete Sign-In Page
Start dev server and visit page:
npm run devNavigate to: http://localhost:5173/sign-in
Check visual appearance:
Should see:
- ✅ Gradient background
- ✅ White card centered
- ✅ Brand logo and tagline
- ✅ "Sign in to your account" title
- ✅ Email and password fields
- ✅ Blue "Sign In" button
- ✅ "Sign up" link at bottom
Test form validation:
- Click "Sign In" with empty fields
- Should see error messages
- Fill email with invalid format
- Should see "Invalid email" error
- Fill both correctly
- Should remove errors
✅ Validation working!
Test submission:
- Fill valid credentials
- Click "Sign In"
- Button should disable and show "Signing in..."
- Check console for logged data
- Button re-enables after submission
✅ Submission working!
Test responsive design:
- Resize browser to mobile width (< 480px)
- Card should remain centered
- Padding should reduce slightly
- Text sizes should adjust
✅ Responsive working!
Add Optional Features
Enhance the sign-in experience:
Add checkbox to remember user:
function SignInForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm({
resolver: zodResolver(signInSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)} className="sign-in-form">
{/* Email field */}
{/* Password field */}
{/* Remember Me */}
<div className="form-extras">
<label className="checkbox-label">
<input
type="checkbox"
{...register('rememberMe')}
className="checkbox-input"
/>
<span className="checkbox-text">Remember me</span>
</label>
<a href="/forgot-password" className="forgot-link">
Forgot password?
</a>
</div>
{/* Submit button */}
</form>
);
}Update schema:
export const signInSchema = z.object({
email: z.string().min(1, 'Email is required').email('Invalid email'),
password: z.string().min(1, 'Password is required').min(8, 'Too short'),
rememberMe: z.boolean().optional(), // ← New field
});Add styles:
.form-extras {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: -0.5rem; /* Pull up slightly */
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
user-select: none;
}
.checkbox-input {
width: 1rem;
height: 1rem;
cursor: pointer;
accent-color: #3b82f6; /* Blue checkmark */
}
.checkbox-text {
font-size: 0.875rem;
color: #374151;
}
.forgot-link {
font-size: 0.875rem;
color: #3b82f6;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.forgot-link:hover {
color: #2563eb;
text-decoration: underline;
}Add link below form:
function SignInForm() {
return (
<form onSubmit={handleSubmit(onSubmit)} className="sign-in-form">
{/* Form fields */}
<button type="submit">Sign In</button>
{/* Forgot password link */}
<p className="alternative-action">
Forgot your password?{' '}
<a href="/reset-password" className="alternative-link">
Reset it here
</a>
</p>
</form>
);
}Add styles:
.alternative-action {
text-align: center;
font-size: 0.875rem;
color: #6b7280;
margin: 1rem 0 0 0;
}
.alternative-link {
color: #3b82f6;
text-decoration: none;
font-weight: 500;
transition: color 0.2s;
}
.alternative-link:hover {
color: #2563eb;
text-decoration: underline;
}Add toggle to reveal password:
import { useState } from 'react';
function SignInForm() {
const [showPassword, setShowPassword] = useState(false);
return (
<form onSubmit={handleSubmit(onSubmit)} className="sign-in-form">
{/* Email field */}
{/* Password field with toggle */}
<div className="form-field">
<label htmlFor="password" className="form-label">
Password
</label>
<div className="password-input-wrapper">
<input
id="password"
type={showPassword ? 'text' : 'password'}
{...register('password')}
className={`form-input ${errors.password ? 'form-input-error' : ''}`}
placeholder="••••••••"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="password-toggle"
aria-label={showPassword ? 'Hide password' : 'Show password'}
>
{showPassword ? '🙈' : '👁️'}
</button>
</div>
{errors.password && (
<p className="form-error">{errors.password.message}</p>
)}
</div>
{/* Submit button */}
</form>
);
}Add styles:
.password-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.password-input-wrapper .form-input {
padding-right: 3rem; /* Space for button */
}
.password-toggle {
position: absolute;
right: 0.75rem;
padding: 0.5rem;
background: none;
border: none;
cursor: pointer;
font-size: 1.25rem;
opacity: 0.6;
transition: opacity 0.2s;
}
.password-toggle:hover {
opacity: 1;
}
.password-toggle:focus-visible {
outline: 2px solid #3b82f6;
outline-offset: 2px;
border-radius: 4px;
}Common Integration Issues
Accessibility Improvements
Every input needs a label:
// ✅ Good: Label with htmlFor
<label htmlFor="email">Email</label>
<input id="email" {...register('email')} />
// ❌ Bad: No connection
<label>Email</label>
<input {...register('email')} />Why this matters:
With htmlFor:
→ Click label → Focuses input
→ Screen reader announces: "Email, edit text"
Without htmlFor:
→ Click label → Nothing happens
→ Screen reader announces: "Edit text" (no context)Label patterns:
// Pattern 1: Wrapping (implicit)
<label>
Email
<input {...register('email')} />
</label>
// Pattern 2: For attribute (explicit, recommended)
<label htmlFor="email">Email</label>
<input id="email" {...register('email')} />Describe errors to screen readers:
<input
id="email"
{...register('email')}
aria-invalid={errors.email ? 'true' : 'false'}
aria-describedby={errors.email ? 'email-error' : undefined}
/>
{errors.email && (
<p id="email-error" className="form-error" role="alert">
{errors.email.message}
</p>
)}What each does:
aria-invalid="true"
// Tells screen reader: "This field has an error"
aria-describedby="email-error"
// Connects error message to input
// Screen reader announces: "Email, invalid, {error message}"
role="alert"
// Announces error immediately when it appearsButton states:
<button
type="submit"
disabled={isSubmitting}
aria-busy={isSubmitting}
>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</button>aria-busy tells screen readers: "This button is busy"
Live regions for dynamic errors:
function SignInForm() {
const { formState: { errors } } = useForm();
return (
<>
{/* Error summary (announced on submit) */}
{Object.keys(errors).length > 0 && (
<div
role="alert"
aria-live="assertive"
className="error-summary"
>
<p>Please fix the following errors:</p>
<ul>
{Object.entries(errors).map(([field, error]) => (
<li key={field}>{error.message}</li>
))}
</ul>
</div>
)}
{/* Form */}
<form>...</form>
</>
);
}ARIA live regions:
role="alert" // Polite announcement
aria-live="assertive" // Immediate announcement
aria-live="polite" // Wait for break in speechError styling:
.error-summary {
padding: 1rem;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 8px;
margin-bottom: 1.5rem;
}
.error-summary p {
color: #991b1b;
font-weight: 600;
margin: 0 0 0.5rem 0;
}
.error-summary ul {
margin: 0;
padding-left: 1.5rem;
color: #dc2626;
}What's Next?
In Lesson 9, we'll:
- Implement the sign-in logic in
onSubmit - Make API call to authentication endpoint
- Handle success and error responses
- Update AuthContext with user data
✅ Lesson Complete! Sign-in page is now fully functional (UI-wise)!
Key Takeaways
- ✅ Import component before using it
- ✅ Remove placeholder when adding real form
- ✅ Test validation after integration
- ✅ Optional features enhance UX (remember me, show password)
- ✅ Social sign-in provides alternatives
- ✅ Accessibility makes form usable by everyone
- ✅ ARIA attributes help screen readers
- ✅ Keyboard navigation must work smoothly
- ✅ Error announcements keep users informed
- ✅ Visual consistency with design system