feat: add user selection flow

This commit is contained in:
Urban Modig
2026-07-24 20:21:33 +02:00
parent 9957383e88
commit 1ec7a72908
22 changed files with 778 additions and 40 deletions

View File

@ -1,48 +1,215 @@
import { useEffect, useState } from 'react'
import { FormEvent, useEffect, useRef, useState } from 'react'
type HealthResponse = {
status: string
type User = {
id: string
name: string
createdAt: string
}
type ApiError = {
code?: string
message?: string
}
const ACTIVE_USER_KEY = 'hemhub.activeUserId'
function App() {
const [backendStatus, setBackendStatus] = useState<string | null>(null)
const [hasError, setHasError] = useState(false)
const [users, setUsers] = useState<User[]>([])
const [activeUser, setActiveUser] = useState<User | null>(null)
const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
const [showCreateUser, setShowCreateUser] = useState(false)
const loadUsers = async () => {
setLoadState('loading')
try {
const response = await fetch('/api/users')
if (!response.ok) {
throw new Error('Kunde inte hämta användare')
}
const loadedUsers = (await response.json()) as User[]
const storedUserId = window.localStorage.getItem(ACTIVE_USER_KEY)
const storedUser = loadedUsers.find((user) => user.id === storedUserId) ?? null
if (storedUserId && !storedUser) {
window.localStorage.removeItem(ACTIVE_USER_KEY)
}
setUsers(loadedUsers)
setActiveUser(storedUser)
setShowCreateUser(loadedUsers.length === 0)
setLoadState('ready')
} catch {
setLoadState('error')
}
}
useEffect(() => {
const loadHealth = async () => {
try {
const response = await fetch('/api/health')
if (!response.ok) {
throw new Error(`Backend svarade med status ${response.status}`)
}
const health = (await response.json()) as HealthResponse
setBackendStatus(health.status)
} catch {
setHasError(true)
}
}
void loadHealth()
void loadUsers()
}, [])
let statusMessage = 'Kontrollerar backend…'
const selectUser = (user: User) => {
window.localStorage.setItem(ACTIVE_USER_KEY, user.id)
setActiveUser(user)
}
if (hasError) {
statusMessage = 'Backend kunde inte nås'
} else if (backendStatus) {
statusMessage = `Backend: ${backendStatus}`
const switchUser = () => {
window.localStorage.removeItem(ACTIVE_USER_KEY)
setActiveUser(null)
setShowCreateUser(false)
}
if (loadState === 'loading') {
return <main>Laddar HemHub</main>
}
if (loadState === 'error') {
return (
<main>
<h1>HemHub</h1>
<p>Kunde inte ansluta till HemHub.</p>
<button type="button" onClick={() => void loadUsers()}>
Försök igen
</button>
</main>
)
}
if (activeUser) {
return (
<main>
<h1>HemHub</h1>
<h2>Hej {activeUser.name}</h2>
<p>Du är vald som aktiv användare.</p>
<button type="button" onClick={switchUser}>
Byt användare
</button>
</main>
)
}
if (showCreateUser) {
return (
<CreateUserForm
hasExistingUsers={users.length > 0}
onCancel={() => setShowCreateUser(false)}
onCreated={(user) => {
setUsers((currentUsers) => [...currentUsers, user])
selectUser(user)
}}
/>
)
}
return (
<main>
<h1>HemHub</h1>
<p>Frontend har startat.</p>
<p aria-live="polite">{statusMessage}</p>
<h2>Vem är du?</h2>
<div className="user-list">
{users.map((user) => (
<button type="button" key={user.id} onClick={() => selectUser(user)}>
{user.name}
</button>
))}
</div>
<button type="button" className="secondary" onClick={() => setShowCreateUser(true)}>
Skapa användare
</button>
</main>
)
}
type CreateUserFormProps = {
hasExistingUsers: boolean
onCancel: () => void
onCreated: (user: User) => void
}
function CreateUserForm({ hasExistingUsers, onCancel, onCreated }: CreateUserFormProps) {
const [name, setName] = useState('')
const [error, setError] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const isSubmittingRef = useRef(false)
const submit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault()
if (isSubmittingRef.current) {
return
}
const trimmedName = name.trim()
if (!trimmedName || [...trimmedName].length > 50) {
setError('Namnet måste innehålla mellan 1 och 50 tecken.')
return
}
setError('')
isSubmittingRef.current = true
setIsSubmitting(true)
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: trimmedName }),
})
if (!response.ok) {
const apiError = (await response.json().catch(() => ({}))) as ApiError
if (
apiError.code === 'INVALID_USER_NAME' ||
apiError.code === 'USER_NAME_ALREADY_EXISTS'
) {
setError(apiError.message ?? 'Det gick inte att skapa användaren.')
} else {
setError('Det gick inte att skapa användaren. Försök igen.')
}
return
}
const createdUser = (await response.json()) as User
onCreated(createdUser)
} catch {
setError('Det gick inte att skapa användaren. Försök igen.')
} finally {
isSubmittingRef.current = false
setIsSubmitting(false)
}
}
return (
<main>
<h1>HemHub</h1>
<h2>Skapa användare</h2>
<form onSubmit={(event) => void submit(event)}>
<label htmlFor="user-name">Namn</label>
<input
id="user-name"
value={name}
disabled={isSubmitting}
onChange={(event) => setName(event.target.value)}
/>
{error && (
<p className="error" role="alert">
{error}
</p>
)}
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Skapar…' : 'Skapa användare'}
</button>
{hasExistingUsers && (
<button type="button" className="secondary" disabled={isSubmitting} onClick={onCancel}>
Tillbaka
</button>
)}
</form>
</main>
)
}
export default App