Files
cli-e2e-mqo4ic70/apps/kanban/src/routes/board.tsx
2026-06-21 20:31:53 +02:00

122 lines
4.0 KiB
TypeScript

import { useState } from 'react'
import { createFileRoute, useRouter } from '@tanstack/react-router'
import { useTranslation } from 'react-i18next'
import { Route as RootRoute } from './__root'
import { formatApiError, makeApi } from '../lib/api'
import { type CardStatus, STATUSES, prettyStatus, relativeTime } from '../lib/kanban'
export const Route = createFileRoute('/board')({
component: BoardPage,
})
function BoardPage() {
const { t } = useTranslation()
const router = useRouter()
const { cards, error } = RootRoute.useLoaderData()
const [title, setTitle] = useState('')
const [status, setStatus] = useState<CardStatus>('todo')
const [pending, setPending] = useState(false)
const [createError, setCreateError] = useState<string | null>(null)
const columns = STATUSES.map((columnStatus) => ({
key: columnStatus,
title: prettyStatus(columnStatus),
cards: cards.filter((card) => card.status === columnStatus),
}))
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault()
if (!title.trim()) return
setPending(true)
setCreateError(null)
try {
await makeApi().invoke('createCard', { title: title.trim(), status })
setTitle('')
await router.invalidate()
} catch (err) {
setCreateError(await formatApiError(err))
} finally {
setPending(false)
}
}
return (
<section className="page-grid">
<div className="panel wide">
<div className="section-heading">
<h2>{t('board.heading')}</h2>
<p>{t('board.sub')}</p>
</div>
{error ? (
<div className="empty-state error-state">{t('board.loadFailed', { error })}</div>
) : (
<div className="board-grid">
{columns.map((column) => (
<div key={column.key} className="column">
<div className="column-header">
<h3>{column.title}</h3>
<span>{column.cards.length}</span>
</div>
<div className="column-cards">
{column.cards.length === 0 ? (
<div className="column-empty">{t('board.columnEmpty')}</div>
) : (
column.cards.map((card) => (
<article key={card.cardId} className="task-card">
<div className="task-card-title">{card.title}</div>
<div className="task-card-meta">
<span>#{card.position}</span>
<span>{relativeTime(card.createdAt)}</span>
</div>
</article>
))
)}
</div>
</div>
))}
</div>
)}
</div>
<div className="panel">
<div className="section-heading">
<h2>{t('board.createHeading')}</h2>
<p>{t('board.createSub')}</p>
</div>
<form className="card-form" onSubmit={handleSubmit}>
<label className="field">
<span>{t('board.titleLabel')}</span>
<input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder={t('board.titlePlaceholder')}
/>
</label>
<label className="field">
<span>{t('board.columnLabel')}</span>
<select
value={status}
onChange={(event) => setStatus(event.target.value as CardStatus)}
>
{STATUSES.map((value) => (
<option key={value} value={value}>
{prettyStatus(value)}
</option>
))}
</select>
</label>
<button type="submit" className="primary-button" disabled={pending || !title.trim()}>
{pending ? t('board.creating') : t('board.createCta')}
</button>
{createError && (
<div className="inline-error">{t('board.createFailed', { error: createError })}</div>
)}
</form>
</div>
</section>
)
}