Phase 4: Rich card features — labels, checklists, comments, attachments, categories
Add full CRUD + UI for all card sub-resources (tags/labels, assignees, checklists with items, comments with markdown, file attachments). Restructure CardModal into a two-column layout with lazy-loaded data. Upgrade card thumbnails with named label pills, urgency-colored due date badges, checklist progress indicators, and SVG icons. Add board categories with filtering on the listing page. Include markdown rendering with DOMPurify sanitization and .prose CSS styles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,16 @@
|
||||
let showCreate = $state(false);
|
||||
let newTitle = $state('');
|
||||
let newVisibility = $state<'PRIVATE' | 'PUBLIC'>('PRIVATE');
|
||||
let newCategoryId = $state('');
|
||||
let creating = $state(false);
|
||||
let error = $state('');
|
||||
let filterCategoryId = $state('');
|
||||
|
||||
const filteredBoards = $derived(
|
||||
filterCategoryId
|
||||
? data.boards.filter((b: any) => b.categoryId === filterCategoryId)
|
||||
: data.boards
|
||||
);
|
||||
|
||||
async function createBoard(e: Event) {
|
||||
e.preventDefault();
|
||||
@@ -19,7 +27,11 @@
|
||||
const res = await fetch('/api/boards', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: newTitle, visibility: newVisibility })
|
||||
body: JSON.stringify({
|
||||
title: newTitle,
|
||||
visibility: newVisibility,
|
||||
...(newCategoryId ? { categoryId: newCategoryId } : {})
|
||||
})
|
||||
});
|
||||
const result = await res.json();
|
||||
if (!res.ok) {
|
||||
@@ -27,6 +39,7 @@
|
||||
return;
|
||||
}
|
||||
newTitle = '';
|
||||
newCategoryId = '';
|
||||
showCreate = false;
|
||||
invalidateAll();
|
||||
} catch {
|
||||
@@ -59,8 +72,8 @@
|
||||
{#if error}
|
||||
<div class="mb-3 rounded bg-red-50 border border-red-200 p-2 text-sm text-red-700">{error}</div>
|
||||
{/if}
|
||||
<form onsubmit={createBoard} class="flex gap-3 items-end">
|
||||
<div class="flex-1">
|
||||
<form onsubmit={createBoard} class="flex gap-3 items-end flex-wrap">
|
||||
<div class="flex-1 min-w-[200px]">
|
||||
<label for="board-title" class="block text-sm font-medium text-gray-700 mb-1">Board title</label>
|
||||
<input
|
||||
id="board-title"
|
||||
@@ -82,6 +95,21 @@
|
||||
<option value="PUBLIC">Public</option>
|
||||
</select>
|
||||
</div>
|
||||
{#if data.categories.length > 0}
|
||||
<div>
|
||||
<label for="board-cat" class="block text-sm font-medium text-gray-700 mb-1">Category</label>
|
||||
<select
|
||||
id="board-cat"
|
||||
bind:value={newCategoryId}
|
||||
class="rounded-lg border border-gray-300 px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{#each data.categories as cat}
|
||||
<option value={cat.id}>{cat.name}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
{/if}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating}
|
||||
@@ -100,7 +128,28 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if data.boards.length === 0}
|
||||
<!-- Category filter -->
|
||||
{#if data.categories.length > 0}
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<span class="text-sm text-gray-500">Filter:</span>
|
||||
<button
|
||||
class="rounded-full px-3 py-1 text-xs {filterCategoryId === '' ? 'bg-[var(--color-primary)] text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'} transition"
|
||||
onclick={() => (filterCategoryId = '')}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{#each data.categories as cat}
|
||||
<button
|
||||
class="rounded-full px-3 py-1 text-xs {filterCategoryId === cat.id ? 'bg-[var(--color-primary)] text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'} transition"
|
||||
onclick={() => (filterCategoryId = cat.id)}
|
||||
>
|
||||
{cat.name}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if filteredBoards.length === 0}
|
||||
<div class="text-center py-16">
|
||||
<svg class="mx-auto w-16 h-16 text-gray-300 mb-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
@@ -108,21 +157,28 @@
|
||||
<rect x="14" y="10" width="7" height="7" rx="1" />
|
||||
<rect x="3" y="13" width="7" height="8" rx="1" />
|
||||
</svg>
|
||||
<p class="text-gray-500">No boards yet.</p>
|
||||
{#if data.user}
|
||||
<p class="text-gray-500">{filterCategoryId ? 'No boards in this category.' : 'No boards yet.'}</p>
|
||||
{#if data.user && !filterCategoryId}
|
||||
<p class="text-gray-400 text-sm mt-1">Create your first board to get started.</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{#each data.boards as board}
|
||||
{#each filteredBoards as board}
|
||||
<a
|
||||
href="/boards/{board.id}"
|
||||
class="group block rounded-lg border border-gray-200 bg-white p-4 shadow-sm hover:shadow-md hover:border-gray-300 transition"
|
||||
>
|
||||
<h2 class="font-semibold text-gray-900 group-hover:text-[var(--color-primary)] transition">
|
||||
{board.title}
|
||||
</h2>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h2 class="font-semibold text-gray-900 group-hover:text-[var(--color-primary)] transition">
|
||||
{board.title}
|
||||
</h2>
|
||||
{#if board.category}
|
||||
<span class="rounded-full bg-gray-100 px-2 py-0.5 text-[10px] text-gray-500 flex-shrink-0">
|
||||
{board.category.name}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3 text-xs text-gray-500">
|
||||
<span>{board._count.columns} columns</span>
|
||||
<span>{board.visibility === 'PUBLIC' ? 'Public' : 'Private'}</span>
|
||||
|
||||
Reference in New Issue
Block a user