import { db } from '$lib/server/db'; import { character, devilFruit, arc, characterOverride } from '$lib/server/db/schema'; import { eq, sql } from 'drizzle-orm'; import { fail } from '@sveltejs/kit'; import type { PageServerLoad, Actions } from './$types'; import { writeFile } from 'fs/promises'; import { join } from 'path'; import { existsSync, mkdirSync } from 'fs'; import { env } from '$env/dynamic/private'; export const load: PageServerLoad = async () => { const [charactersData, devilFruits, arcs, overrides, statusesData, gendersData] = await Promise.all([ db .select({ id: character.id, name: character.name, gender: character.gender, age: character.age, affiliations: character.affiliations, devilFruitId: character.devilFruitId, hakiObservation: character.hakiObservation, hakiArmament: character.hakiArmament, hakiConqueror: character.hakiConqueror, bounty: character.bounty, height: character.height, origin: character.origin, firstAppearance: character.firstAppearance, pictureUrl: character.pictureUrl, epithets: character.epithets, status: character.status, url: character.url, arcId: character.arcId, isInDailyMode: character.isInDailyMode, arcName: arc.name, devilFruitName: devilFruit.name, devilFruitType: devilFruit.type }) .from(character) .leftJoin(arc, eq(character.arcId, arc.id)) .leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id)) .orderBy(character.name), db.select().from(devilFruit).orderBy(devilFruit.name), db.select().from(arc).orderBy(arc.name), db.select().from(characterOverride), db.selectDistinct({ status: character.status }) .from(character) .where(sql`${character.status} IS NOT NULL AND ${character.status} != ''`), db.selectDistinct({ gender: character.gender }) .from(character) .where(sql`${character.gender} IS NOT NULL AND ${character.gender} != ''`) ]); // Create a map of overrides by characterId for easy lookup const overridesMap = new Map(overrides.map((o) => [o.characterId, o])); // Create maps for arcs and devil fruits to lookup names by ID const arcMap = new Map(arcs.map((a) => [a.id, a.name])); const devilFruitMap = new Map(devilFruits.map((f) => [f.id, { name: f.name, type: f.type }])); // Helper function to normalize data (parse JSON arrays) const normalizeArray = (value: any): any => { if (!value) return value; if (Array.isArray(value)) return value; if (typeof value === 'string' && value.includes('[')) { try { return JSON.parse(value); } catch { return value; } } return value; }; // Merge character data with overrides const charactersWithOverrides = charactersData.map((char) => { const override = overridesMap.get(char.id); // Build displayValues by only applying non-null override fields const displayValues = { ...char } as any; if (override) { Object.keys(override).forEach((key) => { if (override[key as keyof typeof override] !== null && key !== 'characterId') { displayValues[key as keyof typeof displayValues] = override[key as keyof typeof override]; } }); // Update arcName if arcId was overridden if (override.arcId !== null && override.arcId !== undefined) { displayValues.arcName = arcMap.get(override.arcId) || null; } // Update devilFruitName and devilFruitType if devilFruitId was overridden if (override.devilFruitId !== null && override.devilFruitId !== undefined) { const fruit = devilFruitMap.get(override.devilFruitId); displayValues.devilFruitName = fruit?.name || null; displayValues.devilFruitType = fruit?.type || null; } } // Pre-normalize arrays (epithets, affiliations) for performance displayValues.epithets = normalizeArray(displayValues.epithets); displayValues.affiliations = normalizeArray(displayValues.affiliations); // Create search text for epithets displayValues.epithetsSearchText = Array.isArray(displayValues.epithets) ? displayValues.epithets.join(' ').toLowerCase() : (displayValues.epithets || '').toLowerCase(); return { ...char, override, displayValues }; }); return { characters: charactersWithOverrides, devilFruits, arcs, availableStatuses: statusesData .map(s => s.status) .filter((s): s is string => !!s) .sort((a, b) => a.localeCompare(b)), availableGenders: gendersData .map(g => g.gender) .filter((g): g is string => !!g) .sort((a, b) => a.localeCompare(b)) }; }; export const actions: Actions = { update: async ({ request, locals }) => { if (!locals.user?.isAdmin) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); const id = formData.get('id') as string; if (!id) { return fail(400, { error: 'Character ID is required' }); } try { const [originalCharacter] = await db .select({ hakiObservation: character.hakiObservation, hakiArmament: character.hakiArmament, hakiConqueror: character.hakiConqueror }) .from(character) .where(eq(character.id, id)) .limit(1); if (!originalCharacter) { return fail(404, { error: 'Character not found' }); } const updates: Record = {}; // Handle file upload const pictureFile = formData.get('pictureFile') as File; const hasUploadedPicture = !!pictureFile && pictureFile.size > 0; if (hasUploadedPicture) { try { const uploadsDir = env.UPLOADS_DIR || join(process.cwd(),'uploads'); if (!existsSync(uploadsDir)) { mkdirSync(uploadsDir, { recursive: true }); } // Get file extension const extension = pictureFile.name.split('.').pop(); const filename = `${id}.${extension}`; const filepath = join(uploadsDir, filename); // Convert file to buffer and save const buffer = Buffer.from(await pictureFile.arrayBuffer()); await writeFile(filepath, buffer); // Update pictureUrl to point to the handler route updates.pictureUrl = `/uploads/${filename}`; } catch (error) { console.error('File upload error:', error); return fail(500, { error: 'Failed to upload file' }); } } formData.forEach((value, key) => { if (key !== 'id' && key !== 'pictureFile') { if (hasUploadedPicture && key === 'pictureUrl') { return; } // Handle integers (age, bounty, height) if (key === 'age' || key === 'bounty' || key === 'height') { const strValue = value as string; updates[key] = strValue && strValue !== '' ? parseInt(strValue) : null; } // Handle text IDs (devilFruitId, arcId) else if (key === 'devilFruitId' || key === 'arcId') { const strValue = value as string; updates[key] = strValue && strValue !== '' ? strValue : null; } // Handle checkboxes (haki fields) after parsing all form data else if (key === 'hakiObservation' || key === 'hakiArmament' || key === 'hakiConqueror') { return; } // Handle strings (name, gender, status, origin, affiliations, epithets, pictureUrl, url, firstAppearance) else { updates[key] = value || null; } } }); const submittedHakiObservation = formData.has('hakiObservation'); const submittedHakiArmament = formData.has('hakiArmament'); const submittedHakiConqueror = formData.has('hakiConqueror'); updates.hakiObservation = submittedHakiObservation === originalCharacter.hakiObservation ? null : submittedHakiObservation; updates.hakiArmament = submittedHakiArmament === originalCharacter.hakiArmament ? null : submittedHakiArmament; updates.hakiConqueror = submittedHakiConqueror === originalCharacter.hakiConqueror ? null : submittedHakiConqueror; // Update or insert into characterOverride table await db .insert(characterOverride) .values({ characterId: id, ...updates }) .onConflictDoUpdate({ target: characterOverride.characterId, set: updates }); return { success: true }; } catch (error) { console.error('Character update error:', error); return fail(500, { error: 'Failed to update character' }); } }, delete: async ({ request, locals }) => { if (!locals.user?.isAdmin) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); const id = formData.get('id') as string; if (!id) { return fail(400, { error: 'Character ID is required' }); } try { await db.delete(character).where(eq(character.id, id)); return { success: true }; } catch (error) { console.error('Character delete error:', error); return fail(500, { error: 'Failed to delete character' }); } }, toggleDailyMode: async ({ request, locals }) => { if (!locals.user?.isAdmin) { return fail(401, { error: 'Unauthorized' }); } const formData = await request.formData(); const id = formData.get('id') as string; const isInDailyMode = formData.get('isInDailyMode') === 'true'; if (!id) { return fail(400, { error: 'Character ID is required' }); } try { await db.update(character) .set({ isInDailyMode }) .where(eq(character.id, id)); return { success: true }; } catch (error) { console.error('Toggle daily mode error:', error); return fail(500, { error: 'Failed to toggle daily mode' }); } } };