import type { PageServerLoad, Actions } from './$types'; import { fail } from '@sveltejs/kit'; import { entryRepository, imageRepository, tagRepository } from '$lib/server/db/repository'; import { saveOriginal, saveThumbnail, ensureDirectories, deleteImage as deleteImageFile } from '$lib/server/images/storage'; import { generateThumbnail } from '$lib/server/images/thumbnails'; import { parseHashtags } from '$lib/utils/parseHashtags'; export const load: PageServerLoad = async ({ url }) => { const showCompleted = url.searchParams.get('showCompleted') === 'true'; const entries = entryRepository.getOrdered({ showCompleted }); // Attach images AND tags to each entry const entriesWithData = entries.map((entry) => ({ ...entry, images: imageRepository.getByEntryId(entry.id), tags: tagRepository.getByEntryId(entry.id) })); // Get all tags for autocomplete const allTags = tagRepository.getAll(); return { entries: entriesWithData, allTags, showCompleted }; }; 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 = {}; const title = formData.get('title'); if (title !== null) { updates.title = title.toString().trim() || null; } let contentChanged = false; 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; contentChanged = true; } 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 }; }, uploadImage: async ({ request }) => { const formData = await request.formData(); const file = formData.get('image') as File; const entryId = formData.get('entryId') as string; if (!file || file.size === 0) { return fail(400, { error: 'No file uploaded' }); } if (!entryId) { return fail(400, { error: 'Entry ID required' }); } // Verify entry exists const entry = entryRepository.getById(entryId); if (!entry) { return fail(404, { error: 'Entry not found' }); } // Validate image type if (!file.type.startsWith('image/')) { return fail(400, { error: 'File must be an image' }); } const ext = file.name.split('.').pop()?.toLowerCase() || 'jpg'; const buffer = Buffer.from(await file.arrayBuffer()); try { await ensureDirectories(); // Generate and save thumbnail const thumbnailBuffer = await generateThumbnail(buffer); // Save to database to get ID const image = imageRepository.create({ entryId, filename: file.name, ext }); // Save original await saveOriginal(image.id, ext, buffer); // Save thumbnail await saveThumbnail(image.id, thumbnailBuffer); return { success: true, imageId: image.id }; } catch (err) { console.error('Upload error:', err); return fail(500, { error: 'Failed to save image' }); } }, deleteImage: async ({ request }) => { const formData = await request.formData(); const imageId = formData.get('imageId')?.toString(); if (!imageId) { return fail(400, { error: 'Image ID required' }); } // Get image to find extension for file deletion const image = imageRepository.getById(imageId); if (!image) { return fail(404, { error: 'Image not found' }); } // Delete files from filesystem try { await deleteImageFile(imageId, image.ext); } catch (err) { console.error('Failed to delete image files:', err); // Continue to delete from database even if files missing } // Delete from database imageRepository.delete(imageId); return { success: true }; }, togglePin: 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 pinned state entryRepository.update(id, { pinned: !existing.pinned }); return { success: true }; }, updateDueDate: async ({ request }) => { const formData = await request.formData(); const id = formData.get('id')?.toString(); const dueDate = formData.get('dueDate')?.toString() || null; if (!id) { return fail(400, { error: 'Entry ID is required' }); } const existing = entryRepository.getById(id); if (!existing) { return fail(404, { error: 'Entry not found' }); } // Update due date (empty string becomes null) entryRepository.update(id, { dueDate: dueDate || null }); return { success: true }; }, updateTags: async ({ request }) => { const formData = await request.formData(); const id = formData.get('id')?.toString(); const tagsJson = formData.get('tags')?.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' }); } try { const tagNames = JSON.parse(tagsJson) as string[]; tagRepository.updateEntryTags(id, tagNames); return { success: true }; } catch { return fail(400, { error: 'Invalid tags format' }); } }, parseTags: async ({ request }) => { const formData = await request.formData(); const id = formData.get('id')?.toString(); const content = formData.get('content')?.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' }); } // Parse hashtags from provided content (or fall back to database content) const textToParse = content ?? existing.content; const newTags = parseHashtags(textToParse); const existingTags = tagRepository.getByEntryId(id).map((t) => t.name.toLowerCase()); const allTags = [...new Set([...existingTags, ...newTags])]; tagRepository.updateEntryTags(id, allTags); return { success: true }; }, removeTag: async ({ request }) => { const formData = await request.formData(); const id = formData.get('id')?.toString(); const tagName = formData.get('tagName')?.toString(); if (!id) { return fail(400, { error: 'Entry ID is required' }); } if (!tagName) { return fail(400, { error: 'Tag name is required' }); } const existing = entryRepository.getById(id); if (!existing) { return fail(404, { error: 'Entry not found' }); } // Get current tags and remove the specified one const currentTags = tagRepository.getByEntryId(id).map((t) => t.name); const updatedTags = currentTags.filter( (t) => t.toLowerCase() !== tagName.toLowerCase() ); tagRepository.updateEntryTags(id, updatedTags); // Clean up orphaned tags tagRepository.cleanupOrphanedTags(); return { success: true }; } };