feat: implement character changes page with new and modified character listings
All checks were successful
Build Docker Image / build (push) Successful in 1m23s

This commit is contained in:
2026-03-03 19:58:02 +01:00
parent 085dae6765
commit 6402c378dd
4 changed files with 246 additions and 25 deletions

View File

@@ -327,8 +327,8 @@ async function importFromJson(): Promise<void> {
} }
} }
} else { } else {
// Check for changes and update scrapeValidation table // Update scrapeValidation table
console.log('Characters table not empty, checking for changes...\n'); console.log('Characters table not empty, updating scrapeValidation table for changes...\n');
for (let i = 0; i < characters.length; i++) { for (let i = 0; i < characters.length; i++) {
const item = characters[i]; const item = characters[i];
@@ -340,33 +340,19 @@ async function importFromJson(): Promise<void> {
.where(eq(character.id, item.id)); .where(eq(character.id, item.id));
lastSql = selectQuery.toSQL(); lastSql = selectQuery.toSQL();
const [dbCharacter] = await selectQuery;
const jsonData = transformCharacterData(item); const jsonData = transformCharacterData(item);
const changed = hasChanged(jsonData, dbCharacter);
if (changed) { const upsertQuery = db
// Update scrapeValidation table with changes .insert(characterScrapeValidation)
const upsertQuery = db .values(jsonData)
.insert(characterScrapeValidation) .onConflictDoUpdate({
.values(jsonData) target: characterScrapeValidation.id,
.onConflictDoUpdate({ set: jsonData
target: characterScrapeValidation.id, });
set: jsonData
});
lastSql = upsertQuery.toSQL();
await upsertQuery;
} else {
// No changes, delete from scrapeValidation if it exists
const deleteQuery = db
.delete(characterScrapeValidation)
.where(eq(characterScrapeValidation.id, item.id));
lastSql = deleteQuery.toSQL();
await deleteQuery;
}
lastSql = upsertQuery.toSQL();
await upsertQuery;
successCount++; successCount++;
process.stdout.write(`\rProcessed: ${successCount}/${characters.length}`); process.stdout.write(`\rProcessed: ${successCount}/${characters.length}`);
} catch (error) { } catch (error) {

View File

@@ -7,6 +7,7 @@
const navItems = [ const navItems = [
{ href: '/admin', label: 'Dashboard', icon: '📊' }, { href: '/admin', label: 'Dashboard', icon: '📊' },
{ href: '/admin/characters', label: 'Characters', icon: '🗣️' }, { href: '/admin/characters', label: 'Characters', icon: '🗣️' },
{ href: '/admin/character-changes', label: 'Changes', icon: '🔄' },
{ href: '/admin/devil-fruits', label: 'Devil Fruits', icon: '🍎' }, { href: '/admin/devil-fruits', label: 'Devil Fruits', icon: '🍎' },
{ href: '/admin/arcs', label: 'Arcs', icon: '📚' }, { href: '/admin/arcs', label: 'Arcs', icon: '📚' },
{ href: '/admin/users', label: 'Users', icon: '👥' }, { href: '/admin/users', label: 'Users', icon: '👥' },

View File

@@ -0,0 +1,88 @@
import { db } from '$lib/server/db';
import { character, characterScrapeValidation } from '$lib/server/db/schema';
export async function load() {
// Get all characters from both tables
const currentCharacters = await db.select().from(character);
const scrapedCharacters = await db.select().from(characterScrapeValidation);
// Create a map for quick lookup
const currentCharMap = new Map(currentCharacters.map(c => [c.id, c]));
// Compare and categorize changes
const changes: {
type: 'new' | 'modified';
id: string;
scraped: (typeof scrapedCharacters)[0];
current?: (typeof currentCharacters)[0];
differences?: Record<string, { current: any; scraped: any }>;
}[] = [];
for (const scraped of scrapedCharacters) {
const current = currentCharMap.get(scraped.id);
if (!current) {
// New character
changes.push({
type: 'new',
id: scraped.id,
scraped
});
} else {
// Check if different
const differences: Record<string, { current: any; scraped: any }> = {};
const fieldsToCompare = [
'name',
'gender',
'age',
'affiliations',
'devilFruitId',
'hakiObservation',
'hakiArmament',
'hakiConqueror',
'bounty',
'height',
'origin',
'firstAppearance',
'pictureUrl',
'epithets',
'status',
'arcId',
'url'
];
for (const field of fieldsToCompare) {
const currentValue = current[field as keyof typeof current];
const scrapedValue = scraped[field as keyof typeof scraped];
// Deep comparison for JSON fields
if (JSON.stringify(currentValue) !== JSON.stringify(scrapedValue)) {
differences[field] = {
current: currentValue,
scraped: scrapedValue
};
}
}
if (Object.keys(differences).length > 0) {
changes.push({
type: 'modified',
id: scraped.id,
scraped,
current,
differences
});
}
}
}
return {
changes: changes.sort((a, b) => {
// Show 'new' first, then 'modified'
if (a.type !== b.type) {
return a.type === 'new' ? -1 : 1;
}
return a.id.localeCompare(b.id);
})
};
}

View File

@@ -0,0 +1,146 @@
<script lang="ts">
import { page } from '$app/stores';
let { data } = $props();
const newCharacters = $derived(data.changes.filter((c: any) => c.type === 'new'));
const modifiedCharacters = $derived(data.changes.filter((c: any) => c.type === 'modified'));
function formatValue(value: any): string {
if (value === null || value === undefined) {
return '—';
}
if (Array.isArray(value)) {
return value.join(', ');
}
if (typeof value === 'boolean') {
return value ? '✓' : '✗';
}
return String(value);
}
function getDifferenceColor(current: any, scraped: any): string {
if (JSON.stringify(current) === JSON.stringify(scraped)) {
return 'text-gray-400';
}
return 'text-amber-300';
}
</script>
<svelte:head>
<title>Admin - Character Changes</title>
</svelte:head>
<div class="space-y-8">
<div>
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 mb-2">Character Changes</h1>
<p class="text-gray-400">Total changes: {newCharacters.length} new, {modifiedCharacters.length} modified</p>
</div>
<!-- New Characters Section -->
{#if newCharacters.length > 0}
<section class="space-y-4">
<h2 class="text-xl font-bold text-emerald-400 uppercase tracking-[0.15em]">
🆕 New Characters ({newCharacters.length})
</h2>
<div class="grid gap-4">
{#each newCharacters as change (change.id)}
<div class="rounded-lg border border-emerald-500/30 bg-emerald-500/5 p-4 space-y-3">
<div class="flex items-center gap-3">
{#if change.scraped.pictureUrl}
<img
src={change.scraped.pictureUrl}
alt={change.scraped.name}
class="w-12 h-12 rounded object-cover"
/>
{/if}
<div>
<h3 class="font-bold text-emerald-300">{change.scraped.name}</h3>
<p class="text-sm text-gray-500">{change.id}</p>
</div>
</div>
<div class="grid grid-cols-2 gap-2 text-sm">
<div>
<span class="text-gray-400">Status:</span>
<span class="ml-2">{formatValue(change.scraped.status)}</span>
</div>
<div>
<span class="text-gray-400">Gender:</span>
<span class="ml-2">{formatValue(change.scraped.gender)}</span>
</div>
<div>
<span class="text-gray-400">Age:</span>
<span class="ml-2">{formatValue(change.scraped.age)}</span>
</div>
<div>
<span class="text-gray-400">Bounty:</span>
<span class="ml-2">{formatValue(change.scraped.bounty)}</span>
</div>
</div>
</div>
{/each}
</div>
</section>
{/if}
<!-- Modified Characters Section -->
{#if modifiedCharacters.length > 0}
<section class="space-y-4">
<h2 class="text-xl font-bold text-amber-400 uppercase tracking-[0.15em]">
✏️ Modified Characters ({modifiedCharacters.length})
</h2>
<div class="grid gap-6">
{#each modifiedCharacters as change (change.id)}
<div class="rounded-lg border border-amber-500/30 bg-amber-500/5 p-4 space-y-4">
<div class="flex items-center gap-3 pb-4 border-b border-amber-500/20">
{#if change.current?.pictureUrl}
<img
src={change.current.pictureUrl}
alt={change.current.name}
class="w-12 h-12 rounded object-cover"
/>
{/if}
<div>
<h3 class="font-bold text-amber-300">{change.current?.name ?? change.scraped.name}</h3>
<p class="text-sm text-gray-500">{change.id}</p>
</div>
</div>
{#if change.differences}
<div class="space-y-3">
{#each Object.entries(change.differences) as [field, diff]}
<div class="bg-slate-900/50 rounded p-3 space-y-1">
<h4 class="text-sm font-semibold text-amber-100 uppercase tracking-widest">{field}</h4>
<div class="grid grid-cols-2 gap-4 text-sm">
<div class="space-y-1">
<p class="text-gray-500">Current:</p>
<p class="font-mono text-gray-300">{formatValue(diff.current)}</p>
</div>
<div class="space-y-1">
<p class="text-gray-500">Scraped:</p>
<p class="font-mono text-emerald-300 font-semibold">{formatValue(diff.scraped)}</p>
</div>
</div>
</div>
{/each}
</div>
{/if}
</div>
{/each}
</div>
</section>
{/if}
{#if newCharacters.length === 0 && modifiedCharacters.length === 0}
<div class="rounded-lg border border-white/10 bg-white/5 p-8 text-center">
<p class="text-gray-400">Aucun changement détecté. Les tables character et characterScrapeValidation sont synchronisées.</p>
</div>
{/if}
</div>
<style>
:global(body) {
background-color: rgb(15, 23, 42);
color: rgb(203, 213, 225);
}
</style>