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:
@@ -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 };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user