feat: remove overrides
Build Docker Image / build (push) Has been cancelled

This commit is contained in:
2026-04-12 02:01:01 +02:00
parent 29297d3773
commit fa14156d82
7 changed files with 1272 additions and 372 deletions
@@ -1,22 +1,32 @@
import { db } from '$lib/server/db';
import { character, devilFruit, arc, characterOverride } from '$lib/server/db/schema';
import { character, devilFruit, arc, type Status } 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';
// 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;
};
export const load: PageServerLoad = async () => {
const [charactersData, devilFruits, arcs, overrides, statusesData, gendersData] = await Promise.all([
let [characters, devilFruits, arcs, statusesData, gendersData] = await Promise.all([
db
.select({
id: character.id,
name: character.name,
gender: character.gender,
age: character.age,
affiliations: character.affiliations,
affiliations: normalizeArray(character.affiliations),
devilFruitId: character.devilFruitId,
hakiObservation: character.hakiObservation,
hakiArmament: character.hakiArmament,
@@ -26,7 +36,7 @@ export const load: PageServerLoad = async () => {
origin: character.origin,
firstAppearance: character.firstAppearance,
pictureUrl: character.pictureUrl,
epithets: character.epithets,
epithets: normalizeArray(character.epithets),
status: character.status,
url: character.url,
arcId: character.arcId,
@@ -41,7 +51,6 @@ export const load: PageServerLoad = async () => {
.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} != ''`),
@@ -50,76 +59,13 @@ export const load: PageServerLoad = async () => {
.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,
characters,
devilFruits,
arcs,
availableStatuses: statusesData
.map(s => s.status)
.filter((s): s is string => !!s)
.filter((s): s is Status => !!s)
.sort((a, b) => a.localeCompare(b)),
availableGenders: gendersData
.map(g => g.gender)
@@ -129,112 +75,6 @@ export const load: PageServerLoad = async () => {
};
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' });
@@ -63,23 +63,22 @@
const matchesSearch =
normalizedQuery === '' ||
char.displayValues.name.toLowerCase().includes(normalizedQuery) ||
char.displayValues.epithetsSearchText.includes(normalizedQuery);
char.name.toLowerCase().includes(normalizedQuery);
const matchesDaily =
filterDaily === 'all' ||
(filterDaily === 'daily' && char.displayValues.isInDailyMode) ||
(filterDaily === 'not-daily' && !char.displayValues.isInDailyMode);
const matchesStatus = filterStatus === 'all' || (char.displayValues.status || '') === filterStatus;
const matchesGender = filterGender === 'all' || (char.displayValues.gender || '') === filterGender;
(filterDaily === 'daily' && char.isInDailyMode) ||
(filterDaily === 'not-daily' && !char.isInDailyMode);
const matchesStatus = filterStatus === 'all' || (char.status || '') === filterStatus;
const matchesGender = filterGender === 'all' || (char.gender || '') === filterGender;
const matchesArc =
filterArc === 'all' ||
String(char.displayValues.arcId ?? '') === filterArc;
String(char.arcId ?? '') === filterArc;
const matchesHaki =
filterHaki === 'all' ||
(filterHaki === 'observation' && !!char.displayValues.hakiObservation) ||
(filterHaki === 'armament' && !!char.displayValues.hakiArmament) ||
(filterHaki === 'conqueror' && !!char.displayValues.hakiConqueror) ||
(filterHaki === 'none' && !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror);
(filterHaki === 'observation' && !!char.hakiObservation) ||
(filterHaki === 'armament' && !!char.hakiArmament) ||
(filterHaki === 'conqueror' && !!char.hakiConqueror) ||
(filterHaki === 'none' && !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror);
return matchesSearch && matchesDaily && matchesStatus && matchesGender && matchesArc && matchesHaki;
});
@@ -277,116 +276,116 @@
{#each filteredCharacters as char (char.id)}
<tr class="border-b border-white/5 hover:bg-slate-800/50">
<!-- Character -->
<td class="px-4 py-4 text-sm text-white w-64 max-w-64 {isFieldOverridden(char, 'name') || isFieldOverridden(char, 'pictureUrl') ? 'bg-amber-500/10' : ''}">
<td class="px-4 py-4 text-sm text-white w-64 max-w-64">
<div class="flex items-center gap-3 min-w-0">
{#if char.displayValues.url}
{#if char.url}
<a
href={"https://onepiece.fandom.com/wiki/" + char.displayValues.url}
href={"https://onepiece.fandom.com/wiki/" + char.url}
target="_blank"
rel="noopener noreferrer"
class="shrink-0 transition-opacity hover:opacity-80"
>
{#if char.displayValues.pictureUrl}
{#if char.pictureUrl}
<img
src={char.displayValues.pictureUrl}
alt={char.displayValues.name}
src={char.pictureUrl}
alt={char.name}
loading="lazy"
class="h-10 w-10 rounded-full object-cover"
/>
{:else}
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-slate-700 text-gray-400">
{char.displayValues.name?.charAt(0).toUpperCase() || '?'}
{char.name?.charAt(0).toUpperCase() || '?'}
</div>
{/if}
</a>
{:else}
{#if char.displayValues.pictureUrl}
{#if char.pictureUrl}
<img
src={char.displayValues.pictureUrl}
alt={char.displayValues.name}
src={char.pictureUrl}
alt={char.name}
loading="lazy"
class="h-10 w-10 shrink-0 rounded-full object-cover"
/>
{:else}
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-700 text-gray-400">
{char.displayValues.name?.charAt(0).toUpperCase() || '?'}
{char.name?.charAt(0).toUpperCase() || '?'}
</div>
{/if}
{/if}
<div class="flex flex-col min-w-0">
{#if char.displayValues.url}
{#if char.url}
<a
href="https://onepiece.fandom.com/wiki/{char.displayValues.url}"
href="https://onepiece.fandom.com/wiki/{char.url}"
target="_blank"
rel="noopener noreferrer"
class="font-medium truncate text-white hover:text-amber-200 hover:underline"
>
{char.displayValues.name}
{char.name}
</a>
{:else}
<span class="font-medium truncate">{char.displayValues.name}</span>
<span class="font-medium truncate">{char.name}</span>
{/if}
{#if char.displayValues.epithets}
{#if char.epithets}
<span class="text-xs text-gray-500 truncate">
{Array.isArray(char.displayValues.epithets)
? char.displayValues.epithets.join(', ')
: char.displayValues.epithets}
{Array.isArray(char.epithets)
? char.epithets.join(', ')
: char.epithets}
</span>
{/if}
</div>
</div>
</td>
<!-- Status -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'status') ? 'bg-amber-500/10' : ''}">{char.displayValues.status || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.status || '-'}</td>
<!-- Gender -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'gender') ? 'bg-amber-500/10' : ''}">{char.displayValues.gender || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.gender || '-'}</td>
<!-- Affiliations -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'affiliations') ? 'bg-amber-500/10' : ''}">
{#if char.displayValues.affiliations}
{#if Array.isArray(char.displayValues.affiliations) && char.displayValues.affiliations.length > 0}
<span class="inline-block" title={char.displayValues.affiliations.join(', ')}>{char.displayValues.affiliations[0]}</span>
<td class="px-4 py-4 text-sm text-gray-400">
{#if char.affiliations}
{#if Array.isArray(char.affiliations) && char.affiliations.length > 0}
<span class="inline-block" title={char.affiliations.join(', ')}>{char.affiliations[0]}</span>
{:else}
{char.displayValues.affiliations}
{char.affiliations}
{/if}
{:else}
-
{/if}
</td>
<!-- Fruit -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'devilFruitId') ? 'bg-amber-500/10' : ''}">{char.displayValues.devilFruitName || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.devilFruitName || '-'}</td>
<!-- Haki -->
<td class="px-4 py-4 text-sm {isFieldOverridden(char, 'hakiObservation') || isFieldOverridden(char, 'hakiArmament') || isFieldOverridden(char, 'hakiConqueror') ? 'bg-amber-500/10' : ''}">
<td class="px-4 py-4 text-sm">
<div class="flex gap-1">
{#if char.displayValues.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
{#if char.displayValues.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if char.displayValues.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror}
{#if char.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
{#if char.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if char.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror}
<span class="text-gray-400">-</span>
{/if}
</div>
</td>
<!-- Bounty -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'bounty') ? 'bg-amber-500/10' : ''}">
{#if char.displayValues.bounty != null}
{formatBounty(char.displayValues.bounty)} ฿
<td class="px-4 py-4 text-sm text-gray-400">
{#if char.bounty != null}
{formatBounty(char.bounty)} ฿
{:else}
-
{/if}
</td>
<!-- Height -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'height') ? 'bg-amber-500/10' : ''}">
{#if char.displayValues.height}
{char.displayValues.height} m
<td class="px-4 py-4 text-sm text-gray-400">
{#if char.height}
{char.height} m
{:else}
-
{/if}
</td>
<!-- Origin -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'origin') ? 'bg-amber-500/10' : ''}">{char.displayValues.origin || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.origin || '-'}</td>
<!-- Arc -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'arcId') || isFieldOverridden(char, 'arcName') ? 'bg-amber-500/10' : ''}">{char.displayValues.arcName || '-'}</td>
<td class="px-4 py-4 text-sm text-gray-400">{char.arcName || '-'}</td>
<!-- Daily Mode -->
<td class="px-4 py-4 text-sm {isFieldOverridden(char, 'isInDailyMode') ? 'bg-amber-500/10' : ''}">
<td class="px-4 py-4 text-sm">
<form
method="POST"
action="?/toggleDailyMode"
@@ -404,11 +403,11 @@
}}
>
<input type="hidden" name="id" value={char.id} />
<input type="hidden" name="isInDailyMode" value={(!char.displayValues.isInDailyMode).toString()} />
<input type="hidden" name="isInDailyMode" value={(!char.isInDailyMode).toString()} />
<label class="flex items-center justify-center cursor-pointer">
<input
type="checkbox"
checked={char.displayValues.isInDailyMode}
checked={char.isInDailyMode}
onchange={(e) => {
const form = e.currentTarget.closest('form');
if (form) form.requestSubmit();
@@ -797,4 +796,4 @@
</div>
</div>
{/if}
</div>
</div>