feat: login page, 401 interceptor, nav sign-out

This commit is contained in:
2026-05-09 12:36:40 -07:00
parent 0d70fb118d
commit fe64f0ead4
3 changed files with 78 additions and 0 deletions
+14
View File
@@ -4,6 +4,8 @@ import Dashboard from './pages/Dashboard'
import MealDetail from './pages/MealDetail'
import Pantry from './pages/Pantry'
import ShoppingList from './pages/ShoppingList'
import Login from './pages/Login'
import { mealPlannerApi } from './api'
const queryClient = new QueryClient()
@@ -26,6 +28,17 @@ function App() {
Shopping List
</Link>
</div>
<div className="flex items-center">
<button
onClick={async () => {
await mealPlannerApi.auth.logout().catch(() => {})
window.location.href = '/login'
}}
className="text-sm text-gray-500 hover:text-gray-900"
>
Sign out
</button>
</div>
</div>
</div>
</nav>
@@ -35,6 +48,7 @@ function App() {
<Route path="/meals/:id" element={<MealDetail />} />
<Route path="/pantry" element={<Pantry />} />
<Route path="/shopping-list" element={<ShoppingList />} />
<Route path="/login" element={<Login />} />
</Routes>
</main>
</div>
+10
View File
@@ -10,6 +10,16 @@ const api = axios.create({
withCredentials: true,
})
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401 && window.location.pathname !== '/login') {
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export const mealPlannerApi = {
auth: {
login: (password: string) => api.post('/auth/login', { password }),
+54
View File
@@ -0,0 +1,54 @@
import { useState } from 'react'
import { mealPlannerApi } from '../api'
export default function Login() {
const [password, setPassword] = useState('')
const [error, setError] = useState('')
const [loading, setLoading] = useState(false)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setLoading(true)
try {
await mealPlannerApi.auth.login(password)
window.location.href = '/'
} catch {
setError('Incorrect password. Try again.')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="bg-white rounded-lg shadow p-8 w-full max-w-sm">
<h1 className="text-2xl font-bold text-gray-900 mb-2 text-center">MealPlanner</h1>
<p className="text-sm text-gray-500 text-center mb-6">Family login</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
type="password"
value={password}
onChange={e => setPassword(e.target.value)}
className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
autoFocus
required
/>
</div>
{error && <p className="text-red-600 text-sm">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 font-medium"
>
{loading ? 'Signing in…' : 'Sign in'}
</button>
</form>
</div>
</div>
)
}