This commit is contained in:
1
drizzle/0002_old_earthquake.sql
Normal file
1
drizzle/0002_old_earthquake.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE `character_override`;
|
||||||
1185
drizzle/meta/0002_snapshot.json
Normal file
1185
drizzle/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
|||||||
"when": 1773697753818,
|
"when": 1773697753818,
|
||||||
"tag": "0001_fuzzy_talisman",
|
"tag": "0001_fuzzy_talisman",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1775950314114,
|
||||||
|
"tag": "0002_old_earthquake",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { arc, character, characterHistory, characterOverride, devilFruit, type Character, type CharacterOverride } from '$lib/server/db/schema';
|
import { arc, character, characterHistory, devilFruit, type Character } from '$lib/server/db/schema';
|
||||||
import { desc, eq, inArray, and } from 'drizzle-orm';
|
import { desc, eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
// Generate or get random seed for daily character selection
|
// Generate or get random seed for daily character selection
|
||||||
const RANDOM_SEED = Math.random();
|
const RANDOM_SEED = Math.random();
|
||||||
@@ -51,104 +51,6 @@ function isNotNullish<T>(value: T | null | undefined): value is T {
|
|||||||
return value !== null && value !== undefined;
|
return value !== null && value !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeCharacterWithOverride(
|
|
||||||
baseCharacter: CharacterWithRelations,
|
|
||||||
overrideRow?: CharacterOverride,
|
|
||||||
relationMaps?: RelationMaps
|
|
||||||
): CharacterWithRelations {
|
|
||||||
if (!overrideRow) {
|
|
||||||
return baseCharacter;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergedCharacter = { ...baseCharacter } as CharacterWithRelations;
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(overrideRow)) {
|
|
||||||
if (key === 'characterId' || key === 'notes') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNotNullish(value)) {
|
|
||||||
(mergedCharacter as Record<string, unknown>)[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relationMaps) {
|
|
||||||
if (mergedCharacter.arcId) {
|
|
||||||
mergedCharacter.arcName = relationMaps.arcNameById.get(mergedCharacter.arcId) ?? null;
|
|
||||||
mergedCharacter.frArcName = relationMaps.arcNameById.get(mergedCharacter.arcId) ?? null;
|
|
||||||
} else {
|
|
||||||
mergedCharacter.arcName = null;
|
|
||||||
mergedCharacter.frArcName = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mergedCharacter.devilFruitId) {
|
|
||||||
const devilFruitData = relationMaps.devilFruitById.get(mergedCharacter.devilFruitId);
|
|
||||||
mergedCharacter.devilFruitName = devilFruitData?.name ?? null;
|
|
||||||
mergedCharacter.devilFruitType = devilFruitData?.type ?? null;
|
|
||||||
} else {
|
|
||||||
mergedCharacter.devilFruitName = null;
|
|
||||||
mergedCharacter.devilFruitType = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergedCharacter;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function applyCharacterOverrides(
|
|
||||||
characters: CharacterWithRelations[]
|
|
||||||
): Promise<CharacterWithRelations[]> {
|
|
||||||
if (characters.length === 0) {
|
|
||||||
return characters;
|
|
||||||
}
|
|
||||||
|
|
||||||
const characterIds = characters.map((currentCharacter) => currentCharacter.id);
|
|
||||||
const overrideRows = await db
|
|
||||||
.select()
|
|
||||||
.from(characterOverride)
|
|
||||||
.where(inArray(characterOverride.characterId, characterIds));
|
|
||||||
|
|
||||||
if (overrideRows.length === 0) {
|
|
||||||
return characters;
|
|
||||||
}
|
|
||||||
|
|
||||||
const overrideByCharacterId = new Map<string, CharacterOverride>(
|
|
||||||
overrideRows.map((overrideRow) => [overrideRow.characterId, overrideRow])
|
|
||||||
);
|
|
||||||
|
|
||||||
const shouldRefreshRelations = overrideRows.some(
|
|
||||||
(overrideRow) => isNotNullish(overrideRow.arcId) || isNotNullish(overrideRow.devilFruitId)
|
|
||||||
);
|
|
||||||
|
|
||||||
let relationMaps: RelationMaps | undefined;
|
|
||||||
|
|
||||||
if (shouldRefreshRelations) {
|
|
||||||
const [allArcs, allDevilFruits] = await Promise.all([
|
|
||||||
db.select({ id: arc.id, name: arc.name }).from(arc),
|
|
||||||
db
|
|
||||||
.select({ id: devilFruit.id, name: devilFruit.name, type: devilFruit.type })
|
|
||||||
.from(devilFruit)
|
|
||||||
]);
|
|
||||||
|
|
||||||
relationMaps = {
|
|
||||||
arcNameById: new Map(allArcs.map((currentArc) => [currentArc.id, currentArc.name])),
|
|
||||||
devilFruitById: new Map(
|
|
||||||
allDevilFruits.map((currentDevilFruit) => [
|
|
||||||
currentDevilFruit.id,
|
|
||||||
{ name: currentDevilFruit.name, type: currentDevilFruit.type }
|
|
||||||
])
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return characters.map((currentCharacter) =>
|
|
||||||
mergeCharacterWithOverride(
|
|
||||||
currentCharacter,
|
|
||||||
overrideByCharacterId.get(currentCharacter.id),
|
|
||||||
relationMaps
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateKey(date: Date): number {
|
export function getDateKey(date: Date): number {
|
||||||
return normalizeDay(date).getTime();
|
return normalizeDay(date).getTime();
|
||||||
}
|
}
|
||||||
@@ -168,26 +70,22 @@ function pickDailyCharacter(characters: CharacterWithRelations[], date: Date): C
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> {
|
export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> {
|
||||||
const characters = (await db
|
return (await db
|
||||||
.select(characterWithRelationsSelect)
|
.select(characterWithRelationsSelect)
|
||||||
.from(character)
|
.from(character)
|
||||||
.leftJoin(arc, eq(character.arcId, arc.id))
|
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
.where(eq(character.isInDailyMode, true))
|
.where(eq(character.isInDailyMode, true))
|
||||||
.all()) as CharacterWithRelations[];
|
.all()) as CharacterWithRelations[];
|
||||||
|
|
||||||
return applyCharacterOverrides(characters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllCharacters(): Promise<CharacterWithRelations[]> {
|
export async function getAllCharacters(): Promise<CharacterWithRelations[]> {
|
||||||
const characters = (await db
|
return (await db
|
||||||
.select(characterWithRelationsSelect)
|
.select(characterWithRelationsSelect)
|
||||||
.from(character)
|
.from(character)
|
||||||
.leftJoin(arc, eq(character.arcId, arc.id))
|
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||||
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
.all()) as CharacterWithRelations[];
|
.all()) as CharacterWithRelations[];
|
||||||
|
|
||||||
return applyCharacterOverrides(characters);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> {
|
export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> {
|
||||||
@@ -203,8 +101,7 @@ export async function getCharacterById(characterId: string): Promise<CharacterWi
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [overriddenCharacter] = await applyCharacterOverrides([found as CharacterWithRelations]);
|
return found as CharacterWithRelations
|
||||||
return overriddenCharacter ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateTodayCharacter(
|
export async function getOrCreateTodayCharacter(
|
||||||
|
|||||||
@@ -66,35 +66,6 @@ export const character = sqliteTable('character', {
|
|||||||
|
|
||||||
export type Character = InferSelectModel<typeof character>;
|
export type Character = InferSelectModel<typeof character>;
|
||||||
|
|
||||||
// Define the character override table schema
|
|
||||||
export const characterOverride = sqliteTable('character_override', {
|
|
||||||
characterId: text('character_id').primaryKey().references(() => character.id, { onDelete: 'cascade' }),
|
|
||||||
name: text('name'),
|
|
||||||
gender: text('gender'),
|
|
||||||
age: integer('age'),
|
|
||||||
affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
|
|
||||||
frAffiliations: text('fr_affiliations', { mode: 'json' }).$type<string[]>(),
|
|
||||||
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id, { onDelete: 'set null' }),
|
|
||||||
hakiObservation: integer('haki_observation', { mode: 'boolean' }),
|
|
||||||
hakiArmament: integer('haki_armament', { mode: 'boolean' }),
|
|
||||||
hakiConqueror: integer('haki_conqueror', { mode: 'boolean' }),
|
|
||||||
bounty: integer('bounty'),
|
|
||||||
height: real('height'),
|
|
||||||
origin: text('origin'),
|
|
||||||
frOrigin: text('fr_origin'),
|
|
||||||
firstAppearance: integer('first_appearance'),
|
|
||||||
pictureUrl: text('picture_url'),
|
|
||||||
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
|
||||||
frEpithets: text('fr_epithets', { mode: 'json' }).$type<string[]>(),
|
|
||||||
status: text('status').$type<Status | null>(),
|
|
||||||
arcId: text('arc_id').references(() => arc.id, { onDelete: 'set null' }),
|
|
||||||
url: text('url'),
|
|
||||||
frUrl: text('fr_url'),
|
|
||||||
notes: text('notes')
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CharacterOverride = InferSelectModel<typeof characterOverride>;
|
|
||||||
|
|
||||||
// Define the character scrape validation table schema
|
// Define the character scrape validation table schema
|
||||||
export const characterScrapeValidation = sqliteTable('character_scrape_validation', {
|
export const characterScrapeValidation = sqliteTable('character_scrape_validation', {
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
|
|||||||
@@ -1,61 +1,8 @@
|
|||||||
import { db } from '$lib/server/db';
|
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 { eq, sql } from 'drizzle-orm';
|
||||||
import { fail } from '@sveltejs/kit';
|
import { fail } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad, Actions } from './$types';
|
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)
|
// Helper function to normalize data (parse JSON arrays)
|
||||||
const normalizeArray = (value: any): any => {
|
const normalizeArray = (value: any): any => {
|
||||||
@@ -71,55 +18,54 @@ export const load: PageServerLoad = async () => {
|
|||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Merge character data with overrides
|
export const load: PageServerLoad = async () => {
|
||||||
const charactersWithOverrides = charactersData.map((char) => {
|
let [characters, devilFruits, arcs, statusesData, gendersData] = await Promise.all([
|
||||||
const override = overridesMap.get(char.id);
|
db
|
||||||
|
.select({
|
||||||
// Build displayValues by only applying non-null override fields
|
id: character.id,
|
||||||
const displayValues = { ...char } as any;
|
name: character.name,
|
||||||
if (override) {
|
gender: character.gender,
|
||||||
Object.keys(override).forEach((key) => {
|
age: character.age,
|
||||||
if (override[key as keyof typeof override] !== null && key !== 'characterId') {
|
affiliations: normalizeArray(character.affiliations),
|
||||||
displayValues[key as keyof typeof displayValues] = override[key as keyof typeof override];
|
devilFruitId: character.devilFruitId,
|
||||||
}
|
hakiObservation: character.hakiObservation,
|
||||||
});
|
hakiArmament: character.hakiArmament,
|
||||||
|
hakiConqueror: character.hakiConqueror,
|
||||||
// Update arcName if arcId was overridden
|
bounty: character.bounty,
|
||||||
if (override.arcId !== null && override.arcId !== undefined) {
|
height: character.height,
|
||||||
displayValues.arcName = arcMap.get(override.arcId) || null;
|
origin: character.origin,
|
||||||
}
|
firstAppearance: character.firstAppearance,
|
||||||
|
pictureUrl: character.pictureUrl,
|
||||||
// Update devilFruitName and devilFruitType if devilFruitId was overridden
|
epithets: normalizeArray(character.epithets),
|
||||||
if (override.devilFruitId !== null && override.devilFruitId !== undefined) {
|
status: character.status,
|
||||||
const fruit = devilFruitMap.get(override.devilFruitId);
|
url: character.url,
|
||||||
displayValues.devilFruitName = fruit?.name || null;
|
arcId: character.arcId,
|
||||||
displayValues.devilFruitType = fruit?.type || null;
|
isInDailyMode: character.isInDailyMode,
|
||||||
}
|
arcName: arc.name,
|
||||||
}
|
devilFruitName: devilFruit.name,
|
||||||
|
devilFruitType: devilFruit.type
|
||||||
// Pre-normalize arrays (epithets, affiliations) for performance
|
})
|
||||||
displayValues.epithets = normalizeArray(displayValues.epithets);
|
.from(character)
|
||||||
displayValues.affiliations = normalizeArray(displayValues.affiliations);
|
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||||
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
// Create search text for epithets
|
.orderBy(character.name),
|
||||||
displayValues.epithetsSearchText = Array.isArray(displayValues.epithets)
|
db.select().from(devilFruit).orderBy(devilFruit.name),
|
||||||
? displayValues.epithets.join(' ').toLowerCase()
|
db.select().from(arc).orderBy(arc.name),
|
||||||
: (displayValues.epithets || '').toLowerCase();
|
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} != ''`)
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...char,
|
characters,
|
||||||
override,
|
|
||||||
displayValues
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
characters: charactersWithOverrides,
|
|
||||||
devilFruits,
|
devilFruits,
|
||||||
arcs,
|
arcs,
|
||||||
availableStatuses: statusesData
|
availableStatuses: statusesData
|
||||||
.map(s => s.status)
|
.map(s => s.status)
|
||||||
.filter((s): s is string => !!s)
|
.filter((s): s is Status => !!s)
|
||||||
.sort((a, b) => a.localeCompare(b)),
|
.sort((a, b) => a.localeCompare(b)),
|
||||||
availableGenders: gendersData
|
availableGenders: gendersData
|
||||||
.map(g => g.gender)
|
.map(g => g.gender)
|
||||||
@@ -129,112 +75,6 @@ export const load: PageServerLoad = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
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 }) => {
|
delete: async ({ request, locals }) => {
|
||||||
if (!locals.user?.isAdmin) {
|
if (!locals.user?.isAdmin) {
|
||||||
return fail(401, { error: 'Unauthorized' });
|
return fail(401, { error: 'Unauthorized' });
|
||||||
|
|||||||
@@ -63,23 +63,22 @@
|
|||||||
|
|
||||||
const matchesSearch =
|
const matchesSearch =
|
||||||
normalizedQuery === '' ||
|
normalizedQuery === '' ||
|
||||||
char.displayValues.name.toLowerCase().includes(normalizedQuery) ||
|
char.name.toLowerCase().includes(normalizedQuery);
|
||||||
char.displayValues.epithetsSearchText.includes(normalizedQuery);
|
|
||||||
const matchesDaily =
|
const matchesDaily =
|
||||||
filterDaily === 'all' ||
|
filterDaily === 'all' ||
|
||||||
(filterDaily === 'daily' && char.displayValues.isInDailyMode) ||
|
(filterDaily === 'daily' && char.isInDailyMode) ||
|
||||||
(filterDaily === 'not-daily' && !char.displayValues.isInDailyMode);
|
(filterDaily === 'not-daily' && !char.isInDailyMode);
|
||||||
const matchesStatus = filterStatus === 'all' || (char.displayValues.status || '') === filterStatus;
|
const matchesStatus = filterStatus === 'all' || (char.status || '') === filterStatus;
|
||||||
const matchesGender = filterGender === 'all' || (char.displayValues.gender || '') === filterGender;
|
const matchesGender = filterGender === 'all' || (char.gender || '') === filterGender;
|
||||||
const matchesArc =
|
const matchesArc =
|
||||||
filterArc === 'all' ||
|
filterArc === 'all' ||
|
||||||
String(char.displayValues.arcId ?? '') === filterArc;
|
String(char.arcId ?? '') === filterArc;
|
||||||
const matchesHaki =
|
const matchesHaki =
|
||||||
filterHaki === 'all' ||
|
filterHaki === 'all' ||
|
||||||
(filterHaki === 'observation' && !!char.displayValues.hakiObservation) ||
|
(filterHaki === 'observation' && !!char.hakiObservation) ||
|
||||||
(filterHaki === 'armament' && !!char.displayValues.hakiArmament) ||
|
(filterHaki === 'armament' && !!char.hakiArmament) ||
|
||||||
(filterHaki === 'conqueror' && !!char.displayValues.hakiConqueror) ||
|
(filterHaki === 'conqueror' && !!char.hakiConqueror) ||
|
||||||
(filterHaki === 'none' && !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror);
|
(filterHaki === 'none' && !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror);
|
||||||
|
|
||||||
return matchesSearch && matchesDaily && matchesStatus && matchesGender && matchesArc && matchesHaki;
|
return matchesSearch && matchesDaily && matchesStatus && matchesGender && matchesArc && matchesHaki;
|
||||||
});
|
});
|
||||||
@@ -277,116 +276,116 @@
|
|||||||
{#each filteredCharacters as char (char.id)}
|
{#each filteredCharacters as char (char.id)}
|
||||||
<tr class="border-b border-white/5 hover:bg-slate-800/50">
|
<tr class="border-b border-white/5 hover:bg-slate-800/50">
|
||||||
<!-- Character -->
|
<!-- 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">
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
{#if char.displayValues.url}
|
{#if char.url}
|
||||||
<a
|
<a
|
||||||
href={"https://onepiece.fandom.com/wiki/" + char.displayValues.url}
|
href={"https://onepiece.fandom.com/wiki/" + char.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="shrink-0 transition-opacity hover:opacity-80"
|
class="shrink-0 transition-opacity hover:opacity-80"
|
||||||
>
|
>
|
||||||
{#if char.displayValues.pictureUrl}
|
{#if char.pictureUrl}
|
||||||
<img
|
<img
|
||||||
src={char.displayValues.pictureUrl}
|
src={char.pictureUrl}
|
||||||
alt={char.displayValues.name}
|
alt={char.name}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="h-10 w-10 rounded-full object-cover"
|
class="h-10 w-10 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex h-10 w-10 items-center justify-center rounded-full bg-slate-700 text-gray-400">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
{#if char.displayValues.pictureUrl}
|
{#if char.pictureUrl}
|
||||||
<img
|
<img
|
||||||
src={char.displayValues.pictureUrl}
|
src={char.pictureUrl}
|
||||||
alt={char.displayValues.name}
|
alt={char.name}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
class="h-10 w-10 shrink-0 rounded-full object-cover"
|
class="h-10 w-10 shrink-0 rounded-full object-cover"
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-700 text-gray-400">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col min-w-0">
|
<div class="flex flex-col min-w-0">
|
||||||
{#if char.displayValues.url}
|
{#if char.url}
|
||||||
<a
|
<a
|
||||||
href="https://onepiece.fandom.com/wiki/{char.displayValues.url}"
|
href="https://onepiece.fandom.com/wiki/{char.url}"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="font-medium truncate text-white hover:text-amber-200 hover:underline"
|
class="font-medium truncate text-white hover:text-amber-200 hover:underline"
|
||||||
>
|
>
|
||||||
{char.displayValues.name}
|
{char.name}
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="font-medium truncate">{char.displayValues.name}</span>
|
<span class="font-medium truncate">{char.name}</span>
|
||||||
{/if}
|
{/if}
|
||||||
{#if char.displayValues.epithets}
|
{#if char.epithets}
|
||||||
<span class="text-xs text-gray-500 truncate">
|
<span class="text-xs text-gray-500 truncate">
|
||||||
{Array.isArray(char.displayValues.epithets)
|
{Array.isArray(char.epithets)
|
||||||
? char.displayValues.epithets.join(', ')
|
? char.epithets.join(', ')
|
||||||
: char.displayValues.epithets}
|
: char.epithets}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- Status -->
|
<!-- 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 -->
|
<!-- 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 -->
|
<!-- Affiliations -->
|
||||||
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'affiliations') ? 'bg-amber-500/10' : ''}">
|
<td class="px-4 py-4 text-sm text-gray-400">
|
||||||
{#if char.displayValues.affiliations}
|
{#if char.affiliations}
|
||||||
{#if Array.isArray(char.displayValues.affiliations) && char.displayValues.affiliations.length > 0}
|
{#if Array.isArray(char.affiliations) && char.affiliations.length > 0}
|
||||||
<span class="inline-block" title={char.displayValues.affiliations.join(', ')}>{char.displayValues.affiliations[0]}</span>
|
<span class="inline-block" title={char.affiliations.join(', ')}>{char.affiliations[0]}</span>
|
||||||
{:else}
|
{:else}
|
||||||
{char.displayValues.affiliations}
|
{char.affiliations}
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<!-- Fruit -->
|
<!-- 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 -->
|
<!-- 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">
|
<div class="flex gap-1">
|
||||||
{#if char.displayValues.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
|
{#if char.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
|
||||||
{#if char.displayValues.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
|
{#if char.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
|
||||||
{#if char.displayValues.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
|
{#if char.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
|
||||||
{#if !char.displayValues.hakiObservation && !char.displayValues.hakiArmament && !char.displayValues.hakiConqueror}
|
{#if !char.hakiObservation && !char.hakiArmament && !char.hakiConqueror}
|
||||||
<span class="text-gray-400">-</span>
|
<span class="text-gray-400">-</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<!-- Bounty -->
|
<!-- Bounty -->
|
||||||
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'bounty') ? 'bg-amber-500/10' : ''}">
|
<td class="px-4 py-4 text-sm text-gray-400">
|
||||||
{#if char.displayValues.bounty != null}
|
{#if char.bounty != null}
|
||||||
{formatBounty(char.displayValues.bounty)} ฿
|
{formatBounty(char.bounty)} ฿
|
||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<!-- Height -->
|
<!-- Height -->
|
||||||
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'height') ? 'bg-amber-500/10' : ''}">
|
<td class="px-4 py-4 text-sm text-gray-400">
|
||||||
{#if char.displayValues.height}
|
{#if char.height}
|
||||||
{char.displayValues.height} m
|
{char.height} m
|
||||||
{:else}
|
{:else}
|
||||||
-
|
-
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<!-- Origin -->
|
<!-- 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 -->
|
<!-- 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 -->
|
<!-- 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
|
<form
|
||||||
method="POST"
|
method="POST"
|
||||||
action="?/toggleDailyMode"
|
action="?/toggleDailyMode"
|
||||||
@@ -404,11 +403,11 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={char.id} />
|
<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">
|
<label class="flex items-center justify-center cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={char.displayValues.isInDailyMode}
|
checked={char.isInDailyMode}
|
||||||
onchange={(e) => {
|
onchange={(e) => {
|
||||||
const form = e.currentTarget.closest('form');
|
const form = e.currentTarget.closest('form');
|
||||||
if (form) form.requestSubmit();
|
if (form) form.requestSubmit();
|
||||||
|
|||||||
Reference in New Issue
Block a user