Files
OnePieceDle/src/routes/(admin)/admin/characters/+page.svelte
whidix ded1c8313d
All checks were successful
Build Docker Image / build (push) Successful in 1m11s
fix: update character link URLs to remove language prefix
2026-03-16 23:15:02 +01:00

801 lines
29 KiB
Svelte

<script lang="ts">
import { enhance } from '$app/forms';
import { formatBounty } from '$lib';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
let searchQuery = $state('');
let filterDaily = $state<'all' | 'daily' | 'not-daily'>('all');
let filterStatus = $state('all');
let filterGender = $state('all');
let filterArc = $state('all');
let filterHaki = $state<'all' | 'observation' | 'armament' | 'conqueror' | 'none'>('all');
let isEditModalOpen = $state(false);
let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
let dailyModeToast = $state<{ type: 'success' | 'error'; text: string } | null>(null);
let selectedChar = $state<any>(null);
const showDailyModeToast = (type: 'success' | 'error', text: string) => {
dailyModeToast = { type, text };
setTimeout(() => {
dailyModeToast = null;
}, 3000);
};
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
if (target.files && target.files.length > 0) {
// Clear pictureUrl when a file is selected
editForm.pictureUrl = '';
}
};
let editForm = $state<any>({
id: '',
name: '',
gender: '',
age: null,
bounty: 0,
height: 0,
origin: '',
affiliations: '',
epithets: '',
pictureUrl: '',
url: '',
devilFruitId: null,
hakiObservation: false,
hakiArmament: false,
hakiConqueror: false,
firstAppearance: '',
arcId: null,
status: ''
});
const filteredCharacters = $derived.by(() => {
return data.characters.filter((char) => {
const normalizedQuery = searchQuery.toLowerCase().trim();
const matchesSearch =
normalizedQuery === '' ||
char.displayValues.name.toLowerCase().includes(normalizedQuery) ||
char.displayValues.epithetsSearchText.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;
const matchesArc =
filterArc === 'all' ||
String(char.displayValues.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);
return matchesSearch && matchesDaily && matchesStatus && matchesGender && matchesArc && matchesHaki;
});
});
const isFieldOverridden = (char: any, field: string) => {
return char.override && char.override[field] !== null && char.override[field] !== undefined;
};
const openEditModal = (char: any) => {
selectedChar = char;
const override = char.override || {};
editForm = {
id: char.id,
name: override.name ?? '',
gender: override.gender ?? '',
age: override.age ?? null,
bounty: override.bounty ?? null,
height: override.height ?? null,
origin: override.origin ?? '',
affiliations: override.affiliations ?? '',
epithets: override.epithets ?? '',
pictureUrl: override.pictureUrl ?? '',
url: override.url ?? '',
devilFruitId: override.devilFruitId !== null && override.devilFruitId !== undefined ? override.devilFruitId : (char.devilFruitId || ''),
hakiObservation: override.hakiObservation ?? char.hakiObservation,
hakiArmament: override.hakiArmament ?? char.hakiArmament,
hakiConqueror: override.hakiConqueror ?? char.hakiConqueror,
firstAppearance: override.firstAppearance ?? '',
arcId: override.arcId !== null && override.arcId !== undefined ? override.arcId : (char.arcId || ''),
status: override.status ?? ''
};
isEditModalOpen = true;
};
const closeModal = () => {
isEditModalOpen = false;
selectedChar = null;
editForm = {
id: '',
name: '',
gender: '',
age: null,
bounty: 0,
height: 0,
origin: '',
affiliations: '',
epithets: '',
pictureUrl: '',
url: '',
devilFruitId: null,
hakiObservation: false,
hakiArmament: false,
hakiConqueror: false,
firstAppearance: '',
arcId: null,
status: ''
};
};
const handleDeleteCharacter = async (id: string) => {
if (!confirm('Are you sure you want to delete this character?')) return;
isSaving = true;
const formData = new FormData();
formData.append('id', id);
try {
const response = await fetch('?/delete', {
method: 'POST',
body: formData
});
if (response.ok) {
location.reload();
} else {
const error = await response.json();
saveMessage = {
type: 'error',
text: error.error || 'Failed to delete character'
};
setTimeout(() => {
saveMessage = null;
}, 3000);
}
} catch (error) {
console.error('Error deleting character:', error);
saveMessage = {
type: 'error',
text: 'Error deleting character'
};
setTimeout(() => {
saveMessage = null;
}, 3000);
} finally {
isSaving = false;
}
};
</script>
<svelte:head>
<title>Characters - Admin - OnePieceDle</title>
</svelte:head>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<h2 class="text-3xl font-bold text-white">Character Management</h2>
<button
class="rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-amber-700"
>
+ Add Character
</button>
</div>
<!-- Filters -->
<div class="flex flex-col gap-4 rounded-lg border border-white/10 bg-slate-800/50 p-4 md:flex-row md:items-center">
<input
type="text"
placeholder="Search characters..."
bind:value={searchQuery}
class="flex-1 rounded-lg bg-slate-700 px-4 py-2 text-sm text-white placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-amber-600"
/>
<select
bind:value={filterStatus}
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
>
<option value="all">All Statuses</option>
{#each data.availableStatuses as status (status)}
<option value={status}>{status}</option>
{/each}
</select>
<select
bind:value={filterGender}
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
>
<option value="all">All Genders</option>
{#each data.availableGenders as gender (gender)}
<option value={gender}>{gender}</option>
{/each}
</select>
<select
bind:value={filterArc}
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
>
<option value="all">All Arcs</option>
{#each data.arcs as arc (arc.id)}
<option value={arc.id}>{arc.name}</option>
{/each}
</select>
<select
bind:value={filterHaki}
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
>
<option value="all">All Haki</option>
<option value="observation">Observation</option>
<option value="armament">Armament</option>
<option value="conqueror">Conqueror</option>
<option value="none">No Haki</option>
</select>
<select
bind:value={filterDaily}
class="rounded-lg bg-slate-700 px-4 py-2 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
>
<option value="all">All Characters</option>
<option value="daily">In Daily Mode</option>
<option value="not-daily">Not in Daily Mode</option>
</select>
</div>
<!-- Characters Table -->
<div class="rounded-lg border border-white/10">
<div class="max-h-[calc(100vh-20rem)] overflow-auto">
<table class="w-full">
<thead class="sticky top-0 bg-slate-800 z-10">
<tr class="border-b border-white/10">
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300 w-64">Character</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Status</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Gender</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Affiliations</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Fruit</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Haki</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Bounty</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Height</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Origin</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Arc</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Daily Mode</th>
<th class="px-4 py-3 text-left text-sm font-semibold text-gray-300">Actions</th>
</tr>
</thead>
<tbody>
{#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' : ''}">
<div class="flex items-center gap-3 min-w-0">
{#if char.displayValues.url}
<a
href={"https://onepiece.fandom.com/wiki/" + char.displayValues.url}
target="_blank"
rel="noopener noreferrer"
class="shrink-0 transition-opacity hover:opacity-80"
>
{#if char.displayValues.pictureUrl}
<img
src={char.displayValues.pictureUrl}
alt={char.displayValues.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() || '?'}
</div>
{/if}
</a>
{:else}
{#if char.displayValues.pictureUrl}
<img
src={char.displayValues.pictureUrl}
alt={char.displayValues.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() || '?'}
</div>
{/if}
{/if}
<div class="flex flex-col min-w-0">
{#if char.displayValues.url}
<a
href="https://onepiece.fandom.com/wiki/{char.displayValues.url}"
target="_blank"
rel="noopener noreferrer"
class="font-medium truncate text-white hover:text-amber-200 hover:underline"
>
{char.displayValues.name}
</a>
{:else}
<span class="font-medium truncate">{char.displayValues.name}</span>
{/if}
{#if char.displayValues.epithets}
<span class="text-xs text-gray-500 truncate">
{Array.isArray(char.displayValues.epithets)
? char.displayValues.epithets.join(', ')
: char.displayValues.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>
<!-- Gender -->
<td class="px-4 py-4 text-sm text-gray-400 {isFieldOverridden(char, 'gender') ? 'bg-amber-500/10' : ''}">{char.displayValues.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>
{:else}
{char.displayValues.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>
<!-- Haki -->
<td class="px-4 py-4 text-sm {isFieldOverridden(char, 'hakiObservation') || isFieldOverridden(char, 'hakiArmament') || isFieldOverridden(char, 'hakiConqueror') ? 'bg-amber-500/10' : ''}">
<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}
<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)} ฿
{: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
{: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>
<!-- 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>
<!-- Daily Mode -->
<td class="px-4 py-4 text-sm {isFieldOverridden(char, 'isInDailyMode') ? 'bg-amber-500/10' : ''}">
<form
method="POST"
action="?/toggleDailyMode"
use:enhance={() => {
return async ({ result, update }) => {
if (result.type === 'success') {
await update();
showDailyModeToast('success', 'Daily mode updated successfully!');
} else if (result.type === 'failure') {
showDailyModeToast('error', (result.data as any)?.error || 'Failed to update daily mode');
} else {
showDailyModeToast('error', 'Failed to update daily mode');
}
};
}}
>
<input type="hidden" name="id" value={char.id} />
<input type="hidden" name="isInDailyMode" value={(!char.displayValues.isInDailyMode).toString()} />
<label class="flex items-center justify-center cursor-pointer">
<input
type="checkbox"
checked={char.displayValues.isInDailyMode}
onchange={(e) => {
const form = e.currentTarget.closest('form');
if (form) form.requestSubmit();
}}
class="w-5 h-5 rounded border-gray-600 bg-slate-700 text-green-500 focus:ring-2 focus:ring-green-500 focus:ring-offset-0 cursor-pointer"
/>
</label>
</form>
</td>
<!-- Actions -->
<td class="px-4 py-4 text-sm">
<div class="flex items-center gap-2">
<button
onclick={() => openEditModal(char)}
class="text-amber-400 hover:text-amber-300 transition-colors"
title="Edit character"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</button>
<button
onclick={() => handleDeleteCharacter(char.id)}
class="text-red-400 hover:text-red-300 transition-colors"
title="Delete character"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>
</div>
{#if filteredCharacters.length === 0}
<div class="rounded-lg border border-white/10 bg-slate-800/50 py-12 text-center">
<p class="text-gray-400">No characters found</p>
</div>
{/if}
{#if dailyModeToast}
<div class="fixed right-6 top-6 z-60">
<div
class={`rounded-lg border px-4 py-3 text-sm font-medium shadow-lg backdrop-blur ${
dailyModeToast.type === 'success'
? 'border-green-500/30 bg-green-900/20 text-green-200'
: 'border-red-500/30 bg-red-900/20 text-red-200'
}`}
>
{dailyModeToast.text}
</div>
</div>
{/if}
<!-- Edit Modal -->
{#if isEditModalOpen}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div class="w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-lg border border-white/10 bg-slate-900 p-6">
<h3 class="text-lg font-bold text-white">Edit Character</h3>
<form
class="mt-6 space-y-4"
method="POST"
action="?/update"
enctype="multipart/form-data"
use:enhance={() => {
isSaving = true;
return async ({ result }) => {
isSaving = false;
if (result.type === 'success') {
saveMessage = { type: 'success', text: 'Character saved successfully!' };
setTimeout(() => {
location.reload();
}, 1000);
} else if (result.type === 'failure') {
saveMessage = { type: 'error', text: (result.data as any)?.error || 'Failed to save character' };
}
setTimeout(() => {
saveMessage = null;
}, 3000);
};
}}
>
<input type="hidden" name="id" value={editForm.id} />
<!-- Basic Information -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-amber-500">Basic Information</h4>
<!-- Name -->
<div>
<label for="char-name" class="block text-sm font-medium text-gray-300 mb-2">Name</label>
<input
type="text"
id="char-name"
name="name"
bind:value={editForm.name}
placeholder={selectedChar?.name || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
<!-- Gender and Age -->
<div class="grid grid-cols-2 gap-3">
<div>
<label for="char-gender" class="block text-sm font-medium text-gray-300 mb-2">Gender</label>
<input
type="text"
id="char-gender"
name="gender"
bind:value={editForm.gender}
placeholder={selectedChar?.gender || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
<div>
<label for="char-age" class="block text-sm font-medium text-gray-300 mb-2">Age</label>
<input
type="number"
id="char-age"
name="age"
bind:value={editForm.age}
placeholder={selectedChar?.age?.toString() || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
</div>
<!-- Status -->
<div>
<label for="char-status" class="block text-sm font-medium text-gray-300 mb-2">Status</label>
<input
type="text"
id="char-status"
name="status"
bind:value={editForm.status}
placeholder={selectedChar?.status || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
</div>
<!-- Physical Attributes -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-amber-500">Physical Attributes</h4>
<div class="grid grid-cols-2 gap-3">
<div>
<label for="char-bounty" class="block text-sm font-medium text-gray-300 mb-2">Bounty</label>
<input
type="number"
id="char-bounty"
name="bounty"
bind:value={editForm.bounty}
placeholder={selectedChar?.bounty?.toString() || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
<div>
<label for="char-height" class="block text-sm font-medium text-gray-300 mb-2">Height (cm)</label>
<input
type="number"
id="char-height"
name="height"
bind:value={editForm.height}
placeholder={selectedChar?.height?.toString() || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
</div>
</div>
<!-- Location & Affiliations -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-amber-500">Location & Affiliations</h4>
<div>
<label for="char-origin" class="block text-sm font-medium text-gray-300 mb-2">Origin</label>
<input
type="text"
id="char-origin"
name="origin"
bind:value={editForm.origin}
placeholder={selectedChar?.origin || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
<div>
<label for="char-affiliations" class="block text-sm font-medium text-gray-300 mb-2">Affiliations</label>
<input
type="text"
id="char-affiliations"
name="affiliations"
bind:value={editForm.affiliations}
placeholder={selectedChar?.affiliations || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
<div>
<label for="char-arc" class="block text-sm font-medium text-gray-300 mb-2">Arc</label>
<select
id="char-arc"
name="arcId"
bind:value={editForm.arcId}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white"
>
<option value="">None</option>
{#each data.arcs as arc (arc.id)}
<option value={arc.id}>{arc.name}</option>
{/each}
</select>
{#if selectedChar?.arcName}
<p class="mt-1 text-xs text-gray-500">Original: {selectedChar.arcName}</p>
{/if}
</div>
</div>
<!-- Powers -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-amber-500">Powers</h4>
<div>
<label for="char-fruit" class="block text-sm font-medium text-gray-300 mb-2">Devil Fruit</label>
<select
id="char-fruit"
name="devilFruitId"
bind:value={editForm.devilFruitId}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white"
>
<option value="">None</option>
{#each data.devilFruits as fruit (fruit.id)}
<option value={fruit.id}>{fruit.name}</option>
{/each}
</select>
{#if selectedChar?.devilFruitName}
<p class="mt-1 text-xs text-gray-500">Original: {selectedChar.devilFruitName}</p>
{/if}
</div>
<div class="space-y-2">
<p class="text-sm font-medium text-gray-300">Haki</p>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2">
<input
type="checkbox"
name="hakiObservation"
bind:checked={editForm.hakiObservation}
class="rounded bg-slate-700"
/>
<span class="text-sm text-gray-300">Observation Haki</span>
{#if selectedChar?.hakiObservation !== undefined}
<span class="text-xs text-gray-500">(Original: {selectedChar.hakiObservation ? 'Yes' : 'No'})</span>
{/if}
</label>
<label class="flex items-center gap-2">
<input
type="checkbox"
name="hakiArmament"
bind:checked={editForm.hakiArmament}
class="rounded bg-slate-700"
/>
<span class="text-sm text-gray-300">Armament Haki</span>
{#if selectedChar?.hakiArmament !== undefined}
<span class="text-xs text-gray-500">(Original: {selectedChar.hakiArmament ? 'Yes' : 'No'})</span>
{/if}
</label>
<label class="flex items-center gap-2">
<input
type="checkbox"
name="hakiConqueror"
bind:checked={editForm.hakiConqueror}
class="rounded bg-slate-700"
/>
<span class="text-sm text-gray-300">Conqueror's Haki</span>
{#if selectedChar?.hakiConqueror !== undefined}
<span class="text-xs text-gray-500">(Original: {selectedChar.hakiConqueror ? 'Yes' : 'No'})</span>
{/if}
</label>
</div>
</div>
</div>
<!-- Timeline -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-amber-500">Timeline</h4>
<div>
<label for="char-first-appearance" class="block text-sm font-medium text-gray-300 mb-2">First Appearance</label>
<input
type="text"
id="char-first-appearance"
name="firstAppearance"
bind:value={editForm.firstAppearance}
placeholder={selectedChar?.firstAppearance || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
</div>
<!-- Media -->
<div class="space-y-4">
<h4 class="text-sm font-semibold text-amber-500">Media & Details</h4>
<div>
<label for="char-epithets" class="block text-sm font-medium text-gray-300 mb-2">Epithets</label>
<input
type="text"
id="char-epithets"
name="epithets"
bind:value={editForm.epithets}
placeholder={selectedChar?.epithets || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
<div>
<label for="char-picture-url" class="block text-sm font-medium text-gray-300 mb-2">Picture URL</label>
<input
type="url"
id="char-picture-url"
name="pictureUrl"
bind:value={editForm.pictureUrl}
placeholder={selectedChar?.pictureUrl || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
<div>
<label for="char-picture-file" class="block text-sm font-medium text-gray-300 mb-2">Or Upload Picture</label>
<input
type="file"
id="char-picture-file"
name="pictureFile"
accept="image/*"
onchange={handleFileSelect}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white file:mr-4 file:py-2 file:px-4 file:rounded file:border-0 file:text-sm file:font-semibold file:bg-amber-600 file:text-white hover:file:bg-amber-700"
/>
<p class="mt-1 text-xs text-gray-400">File will be saved as {selectedChar?.id}.jpg/png/etc</p>
</div>
<div>
<label for="char-url" class="block text-sm font-medium text-gray-300 mb-2">Fandom URL</label>
<input
type="url"
id="char-url"
name="url"
bind:value={editForm.url}
placeholder={selectedChar?.url || ''}
class="w-full rounded-lg border border-gray-500 bg-slate-800 px-3 py-2 text-white placeholder:text-gray-400/40"
/>
</div>
</div>
<div class="flex gap-3 pt-4">
<button
type="button"
onclick={closeModal}
disabled={isSaving}
class="flex-1 rounded-lg border border-gray-500 px-4 py-2 text-sm font-medium text-gray-300 transition-colors hover:bg-slate-700 disabled:opacity-50"
>
Cancel
</button>
<button
type="submit"
disabled={isSaving}
class="flex-1 rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-amber-700 disabled:opacity-50"
>
{isSaving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</form>
{#if saveMessage}
<div
class={`mt-4 rounded-lg p-3 text-sm font-medium ${
saveMessage.type === 'success'
? 'border border-green-500/50 bg-green-500/10 text-green-300'
: 'border border-red-500/50 bg-red-500/10 text-red-300'
}`}
>
{saveMessage.text}
</div>
{/if}
</div>
</div>
{/if}
</div>