284 lines
9.1 KiB
TypeScript
284 lines
9.1 KiB
TypeScript
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<string, any> = {};
|
|
|
|
// 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' });
|
|
}
|
|
}
|
|
};
|