diff --git a/src/pages/admin/AdminLogin.module.css b/src/pages/admin/AdminLogin.module.css new file mode 100644 index 0000000..7d21443 --- /dev/null +++ b/src/pages/admin/AdminLogin.module.css @@ -0,0 +1,49 @@ +.page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--color-cream); + padding: var(--space-lg); +} + +.form { + display: flex; + flex-direction: column; + gap: var(--space-md); + width: 100%; + max-width: 320px; +} + +.field { + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.field label { + color: var(--color-charcoal); +} + +.field input { + padding: var(--space-sm); + border: 1px solid var(--color-gold); + background: white; +} + +.error { + color: #a33; +} + +.form button { + background: var(--color-charcoal); + color: var(--color-cream); + border: none; + padding: var(--space-sm) var(--space-md); + cursor: pointer; +} + +.form button:disabled { + opacity: 0.6; + cursor: default; +} diff --git a/src/pages/admin/AdminLogin.test.tsx b/src/pages/admin/AdminLogin.test.tsx new file mode 100644 index 0000000..8a2b558 --- /dev/null +++ b/src/pages/admin/AdminLogin.test.tsx @@ -0,0 +1,49 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { MemoryRouter } from 'react-router-dom' +import { describe, it, expect, vi, beforeEach } from 'vitest' + +const signInMock = vi.fn() +const navigateMock = vi.fn() + +vi.mock('../../lib/supabase', () => ({ + supabase: { auth: { signInWithPassword: (...args: unknown[]) => signInMock(...args) } }, +})) + +vi.mock('react-router-dom', async () => { + const actual = await vi.importActual('react-router-dom') + return { ...actual, useNavigate: () => navigateMock } +}) + +import AdminLogin from './AdminLogin' + +describe('AdminLogin', () => { + beforeEach(() => { + signInMock.mockReset() + navigateMock.mockReset() + }) + + it('signs in and navigates to the admin dashboard on success', async () => { + signInMock.mockResolvedValue({ error: null }) + render() + + await userEvent.type(screen.getByLabelText(/email/i), 'mira@example.com') + await userEvent.type(screen.getByLabelText(/password/i), 'correct-password') + await userEvent.click(screen.getByRole('button', { name: /sign in/i })) + + expect(signInMock).toHaveBeenCalledWith({ email: 'mira@example.com', password: 'correct-password' }) + expect(navigateMock).toHaveBeenCalledWith('/admin/products') + }) + + it('shows an error message on failed sign-in and does not navigate', async () => { + signInMock.mockResolvedValue({ error: { message: 'Invalid login credentials' } }) + render() + + await userEvent.type(screen.getByLabelText(/email/i), 'mira@example.com') + await userEvent.type(screen.getByLabelText(/password/i), 'wrong-password') + await userEvent.click(screen.getByRole('button', { name: /sign in/i })) + + expect(await screen.findByText(/incorrect email or password/i)).toBeInTheDocument() + expect(navigateMock).not.toHaveBeenCalled() + }) +}) diff --git a/src/pages/admin/AdminLogin.tsx b/src/pages/admin/AdminLogin.tsx new file mode 100644 index 0000000..1861b16 --- /dev/null +++ b/src/pages/admin/AdminLogin.tsx @@ -0,0 +1,59 @@ +import { useState, type FormEvent } from 'react' +import { useNavigate } from 'react-router-dom' +import { supabase } from '../../lib/supabase' +import styles from './AdminLogin.module.css' + +function AdminLogin() { + const navigate = useNavigate() + const [email, setEmail] = useState('') + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [submitting, setSubmitting] = useState(false) + + async function handleSubmit(event: FormEvent) { + event.preventDefault() + setSubmitting(true) + setError('') + const { error: signInError } = await supabase.auth.signInWithPassword({ email, password }) + setSubmitting(false) + if (signInError) { + setError('Incorrect email or password.') + return + } + navigate('/admin/products') + } + + return ( +
+
+

Admin Login

+
+ + setEmail(e.target.value)} + required + /> +
+
+ + setPassword(e.target.value)} + required + /> +
+ {error &&

{error}

} + +
+
+ ) +} + +export default AdminLogin