import { FormEvent, MouseEvent, useEffect, useRef, useState } from 'react' type TaskStatus = 'WAITING' | 'IN_PROGRESS' | 'COMPLETED' type Task = { id: string title: string description: string | null status: TaskStatus createdAt: string } type ApiError = { message?: string } type TaskBoardProps = { activeUserName: string onLogOut: () => void } const columns: { status: TaskStatus; title: string }[] = [ { status: 'WAITING', title: 'Väntande' }, { status: 'IN_PROGRESS', title: 'Pågående' }, { status: 'COMPLETED', title: 'Klart' }, ] function TaskBoard({ activeUserName, onLogOut }: TaskBoardProps) { const [tasks, setTasks] = useState([]) const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading') const [showCreateTask, setShowCreateTask] = useState(false) const loadTasks = async () => { setLoadState('loading') try { const response = await fetch('/api/tasks') if (!response.ok) { throw new Error('Kunde inte hämta uppgifter') } setTasks((await response.json()) as Task[]) setLoadState('ready') } catch { setLoadState('error') } } useEffect(() => { void loadTasks() }, []) return (

HemHub

Uppgifter

{activeUserName}
{loadState === 'loading' && 'Laddar uppgifter…'} {loadState === 'error' && ( <> Kunde inte hämta uppgifter. )}
{columns.map((column) => (

{column.title}

{tasks .filter((task) => task.status === column.status) .map((task) => (

{task.title}

{task.description &&

{task.description}

}
))}
))}
{showCreateTask && ( setShowCreateTask(false)} onCreated={(task) => { setTasks((currentTasks) => [...currentTasks, task]) setShowCreateTask(false) }} /> )}
) } type CreateTaskModalProps = { onClose: () => void onCreated: (task: Task) => void } function CreateTaskModal({ onClose, onCreated }: CreateTaskModalProps) { const [title, setTitle] = useState('') const [description, setDescription] = useState('') const [error, setError] = useState('') const [isSubmitting, setIsSubmitting] = useState(false) const isSubmittingRef = useRef(false) useEffect(() => { const closeOnEscape = (event: KeyboardEvent) => { if (event.key === 'Escape' && !isSubmittingRef.current) { onClose() } } window.addEventListener('keydown', closeOnEscape) return () => window.removeEventListener('keydown', closeOnEscape) }, [onClose]) const closeFromBackdrop = (event: MouseEvent) => { if (event.target === event.currentTarget && !isSubmittingRef.current) { onClose() } } const submit = async (event: FormEvent) => { event.preventDefault() if (isSubmittingRef.current) { return } const trimmedTitle = title.trim() const trimmedDescription = description.trim() if (!trimmedTitle || [...trimmedTitle].length > 100) { setError('Titeln måste innehålla mellan 1 och 100 tecken.') return } if ([...trimmedDescription].length > 500) { setError('Beskrivningen får innehålla högst 500 tecken.') return } setError('') isSubmittingRef.current = true setIsSubmitting(true) try { const response = await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ title: trimmedTitle, description: trimmedDescription || null, }), }) if (!response.ok) { const apiError = (await response.json().catch(() => ({}))) as ApiError setError(apiError.message ?? 'Det gick inte att skapa uppgiften. Försök igen.') return } onCreated((await response.json()) as Task) } catch { setError('Det gick inte att skapa uppgiften. Försök igen.') } finally { isSubmittingRef.current = false setIsSubmitting(false) } } return (

Skapa ny uppgift

void submit(event)}> setTitle(event.target.value)} />