shadimage

Login Form

Clean and accessible login form with email/password authentication and Google OAuth option

components/login-form/login-form.tsx

'use client';

import { useState } from 'react';

import {
  Box,
  Button,
  Card,
  Link,
  Stack,
  TextField,
  Typography,
} from '@mui/material';

interface FormData {
  email: string;
  password: string;
}

interface FormErrors {
  email?: string;
  password?: string;
}

export function LoginForm() {
  const [formData, setFormData] = useState<FormData>({
    email: 'm@example.com',
    password: '',
  });
  const [errors, setErrors] = useState<FormErrors>({});
  const [isLoading, setIsLoading] = useState(false);

  const handleInputChange =
    (field: keyof FormData) => (event: React.ChangeEvent<HTMLInputElement>) => {
      setFormData((prev) => ({
        ...prev,
        [field]: event.target.value,
      }));
      // Clear error when user starts typing
      if (errors[field]) {
        setErrors((prev) => ({
          ...prev,
          [field]: undefined,
        }));
      }
    };

  const validateForm = (): boolean => {
    const newErrors: FormErrors = {};

    if (!formData.email) {
      newErrors.email = 'Email is required';
    } else if (!/\S+@\S+\.\S+/.test(formData.email)) {
      newErrors.email = 'Please enter a valid email address';
    }

    if (!formData.password) {
      newErrors.password = 'Password is required';
    } else if (formData.password.length < 6) {
      newErrors.password = 'Password must be at least 6 characters';
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = async (event: React.FormEvent) => {
    event.preventDefault();

    if (!validateForm()) {
      return;
    }

    setIsLoading(true);
    // Simulate API call
    await new Promise((resolve) => setTimeout(resolve, 1000));
    setIsLoading(false);

    // Handle successful login
    console.log('Login successful', formData);
  };

  const handleGoogleLogin = () => {
    console.log('Login with Google');
  };

  return (
    <Card
      sx={(theme) => ({
        mx: 'auto',
        p: 4,
        borderRadius: 2,
        maxWidth: 400,
        ...theme.applyStyles('dark', {
          bgcolor: 'grey.900',
        }),
      })}
    >
      <Stack spacing={3}>
        <Stack
          spacing={1}
          sx={{
            textAlign: 'center',
          }}
        >
          <Typography variant="h3" component="h1">
            Login to your account
          </Typography>
          <Typography
            variant="body1"
            sx={{
              color: 'text.secondary',
            }}
          >
            Enter your email below to login to your account
          </Typography>
        </Stack>

        <Box component="form" onSubmit={handleSubmit}>
          <Stack spacing={2}>
            <TextField
              fullWidth
              required
              label="Email"
              type="email"
              variant="outlined"
              value={formData.email}
              onChange={handleInputChange('email')}
              error={!!errors.email}
              helperText={errors.email}
              autoComplete="email"
            />

            <Box>
              <Stack
                direction="row"
                sx={{
                  justifyContent: 'space-between',
                  alignItems: 'center',
                  mb: 1,
                }}
              >
                <Typography
                  variant="body2"
                  component="label"
                  htmlFor="password-input"
                >
                  Password
                </Typography>
                <Link
                  href="#"
                  variant="body2"
                  onClick={(e) => {
                    e.preventDefault();
                    console.log('Forgot password clicked');
                  }}
                  sx={{ textDecoration: 'none' }}
                >
                  Forgot your password?
                </Link>
              </Stack>
              <TextField
                id="password-input"
                fullWidth
                required
                type="password"
                variant="outlined"
                value={formData.password}
                onChange={handleInputChange('password')}
                error={!!errors.password}
                helperText={errors.password}
                autoComplete="current-password"
              />
            </Box>

            <Button
              type="submit"
              variant="contained"
              size="large"
              fullWidth
              disabled={isLoading}
              sx={{ py: 1.5 }}
            >
              {isLoading ? 'Signing in...' : 'Login'}
            </Button>

            <Button
              variant="outlined"
              size="large"
              fullWidth
              onClick={handleGoogleLogin}
              sx={{ py: 1.5 }}
            >
              Login with Google
            </Button>
          </Stack>
        </Box>

        <Typography
          variant="body2"
          sx={{
            textAlign: 'center',
            color: 'text.secondary',
          }}
        >
          Don&apos;t have an account?{' '}
          <Link
            href="#"
            onClick={(e) => {
              e.preventDefault();
              console.log('Sign up clicked');
            }}
            sx={{ textDecoration: 'underline' }}
          >
            Sign up
          </Link>
        </Typography>
      </Stack>
    </Card>
  );
}
Where it goes
login
Visual style
minimal
Use cases
saas-landing

DependenciesBundle weight · light

@mui/material@emotion/react@emotion/styled