feat(02-01): add CRUD form actions

- Add create action with content validation (title optional, type defaults to thought)
- Add update action with field-by-field updates and validation
- Add delete action with existence check
- Add toggleComplete action to toggle between open/done
- Use getOrdered() in load function for tasks-first ordering
- Update page component to use new data structure
This commit is contained in:
Thomas Richter
2026-01-29 11:02:43 +01:00
parent 87bf928c12
commit 9a449228b7
2 changed files with 133 additions and 28 deletions

View File

@@ -1,24 +1,129 @@
import type { PageServerLoad } from './$types'; import type { PageServerLoad, Actions } from './$types';
import { fail } from '@sveltejs/kit';
import { entryRepository } from '$lib/server/db/repository'; import { entryRepository } from '$lib/server/db/repository';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
const count = entryRepository.count(); const entries = entryRepository.getOrdered({ showCompleted: false });
// Create a test entry if none exist (for verification)
let testEntry = null;
if (count === 0) {
testEntry = entryRepository.create({
title: 'Foundation Test',
content: 'This entry was created to verify the foundation is working.',
type: 'thought'
});
}
const entries = entryRepository.getAll({ limit: 5 });
return { return {
dbStatus: 'connected', entries
entryCount: testEntry ? count + 1 : count,
recentEntries: entries
}; };
}; };
export const actions: Actions = {
create: async ({ request }) => {
const formData = await request.formData();
const title = formData.get('title')?.toString().trim() || undefined;
const content = formData.get('content')?.toString().trim() || '';
const type = formData.get('type')?.toString() as 'task' | 'thought' | undefined;
// Validate content is not empty
if (!content) {
return fail(400, {
error: 'Content is required',
title: title || '',
content: '',
type: type || 'thought'
});
}
const entry = entryRepository.create({
title,
content,
type: type || 'thought'
});
return { success: true, entryId: entry.id };
},
update: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id')?.toString();
if (!id) {
return fail(400, { error: 'Entry ID is required' });
}
const existing = entryRepository.getById(id);
if (!existing) {
return fail(404, { error: 'Entry not found' });
}
// Collect fields to update
const updates: Record<string, string | boolean | null> = {};
const title = formData.get('title');
if (title !== null) {
updates.title = title.toString().trim() || null;
}
const content = formData.get('content');
if (content !== null) {
const contentStr = content.toString().trim();
if (contentStr === '') {
return fail(400, { error: 'Content cannot be empty' });
}
updates.content = contentStr;
}
const type = formData.get('type');
if (type !== null) {
const typeStr = type.toString();
if (typeStr !== 'task' && typeStr !== 'thought') {
return fail(400, { error: 'Invalid type' });
}
updates.type = typeStr;
}
const status = formData.get('status');
if (status !== null) {
const statusStr = status.toString();
if (statusStr !== 'open' && statusStr !== 'done' && statusStr !== 'archived') {
return fail(400, { error: 'Invalid status' });
}
updates.status = statusStr;
}
entryRepository.update(id, updates);
return { success: true };
},
delete: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id')?.toString();
if (!id) {
return fail(400, { error: 'Entry ID is required' });
}
const existing = entryRepository.getById(id);
if (!existing) {
return fail(404, { error: 'Entry not found' });
}
entryRepository.delete(id);
return { success: true };
},
toggleComplete: async ({ request }) => {
const formData = await request.formData();
const id = formData.get('id')?.toString();
if (!id) {
return fail(400, { error: 'Entry ID is required' });
}
const existing = entryRepository.getById(id);
if (!existing) {
return fail(404, { error: 'Entry not found' });
}
// Toggle status between 'open' and 'done'
const newStatus = existing.status === 'done' ? 'open' : 'done';
entryRepository.update(id, { status: newStatus });
return { success: true };
}
};

View File

@@ -5,7 +5,7 @@
<main class="min-h-screen bg-gray-50 p-8"> <main class="min-h-screen bg-gray-50 p-8">
<div class="max-w-2xl mx-auto"> <div class="max-w-2xl mx-auto">
<h1 class="text-3xl font-bold text-gray-900 mb-2">TaskPlanner</h1> <h1 class="text-3xl font-bold text-gray-900 mb-2">TaskPlanner</h1>
<p class="text-gray-600 mb-8">Foundation Phase Complete</p> <p class="text-gray-600 mb-8">Core CRUD Phase - Form Actions Ready</p>
<div class="bg-white rounded-lg shadow p-6 mb-6"> <div class="bg-white rounded-lg shadow p-6 mb-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">System Status</h2> <h2 class="text-lg font-semibold text-gray-800 mb-4">System Status</h2>
@@ -13,34 +13,34 @@
<div class="space-y-3"> <div class="space-y-3">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-green-500"></span> <span class="w-3 h-3 rounded-full bg-green-500"></span>
<span class="text-gray-700">Database: {data.dbStatus}</span> <span class="text-gray-700">Form actions: create, update, delete, toggleComplete</span>
</div> </div>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-green-500"></span> <span class="w-3 h-3 rounded-full bg-green-500"></span>
<span class="text-gray-700">Entries in database: {data.entryCount}</span> <span class="text-gray-700">Entries loaded: {data.entries.length}</span>
</div>
<div class="flex items-center gap-2">
<span class="w-3 h-3 rounded-full bg-green-500"></span>
<span class="text-gray-700">Repository layer: operational</span>
</div> </div>
</div> </div>
</div> </div>
{#if data.recentEntries.length > 0} {#if data.entries.length > 0}
<div class="bg-white rounded-lg shadow p-6"> <div class="bg-white rounded-lg shadow p-6">
<h2 class="text-lg font-semibold text-gray-800 mb-4">Recent Entries</h2> <h2 class="text-lg font-semibold text-gray-800 mb-4">Entries (tasks first, newest within type)</h2>
<ul class="space-y-2"> <ul class="space-y-2">
{#each data.recentEntries as entry} {#each data.entries as entry}
<li class="p-3 bg-gray-50 rounded"> <li class="p-3 bg-gray-50 rounded">
<div class="font-medium text-gray-900">{entry.title || '(no title)'}</div> <div class="font-medium text-gray-900">{entry.title || '(no title)'}</div>
<div class="text-sm text-gray-600">{entry.content}</div> <div class="text-sm text-gray-600">{entry.content}</div>
<div class="text-xs text-gray-400 mt-1"> <div class="text-xs text-gray-400 mt-1">
Type: {entry.type} | Created: {new Date(entry.createdAt).toLocaleString()} Type: {entry.type} | Status: {entry.status} | Created: {new Date(entry.createdAt).toLocaleString()}
</div> </div>
</li> </li>
{/each} {/each}
</ul> </ul>
</div> </div>
{:else}
<div class="bg-white rounded-lg shadow p-6">
<p class="text-gray-500">No entries yet. Use form actions to create entries.</p>
</div>
{/if} {/if}
</div> </div>
</main> </main>