feat: add admin login page
This commit is contained in:
49
src/pages/admin/AdminLogin.module.css
Normal file
49
src/pages/admin/AdminLogin.module.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
49
src/pages/admin/AdminLogin.test.tsx
Normal file
49
src/pages/admin/AdminLogin.test.tsx
Normal file
@@ -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(<MemoryRouter><AdminLogin /></MemoryRouter>)
|
||||||
|
|
||||||
|
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(<MemoryRouter><AdminLogin /></MemoryRouter>)
|
||||||
|
|
||||||
|
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()
|
||||||
|
})
|
||||||
|
})
|
||||||
59
src/pages/admin/AdminLogin.tsx
Normal file
59
src/pages/admin/AdminLogin.tsx
Normal file
@@ -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 (
|
||||||
|
<div className={styles.page}>
|
||||||
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
|
<h1>Admin Login</h1>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="admin-email">Email</label>
|
||||||
|
<input
|
||||||
|
id="admin-email"
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.field}>
|
||||||
|
<label htmlFor="admin-password">Password</label>
|
||||||
|
<input
|
||||||
|
id="admin-password"
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
<button type="submit" disabled={submitting}>
|
||||||
|
{submitting ? 'Signing in…' : 'Sign in'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AdminLogin
|
||||||
Reference in New Issue
Block a user