feat: enhance character status extraction and update schema for nullable status
All checks were successful
Build Docker Image / build (push) Successful in 1m22s
All checks were successful
Build Docker Image / build (push) Successful in 1m22s
This commit is contained in:
@@ -659,9 +659,9 @@ function extractOrigin($: cheerio.CheerioAPI): string | null {
|
|||||||
/**
|
/**
|
||||||
* Extract status from infobox
|
* Extract status from infobox
|
||||||
*/
|
*/
|
||||||
function extractStatus($: cheerio.CheerioAPI): string {
|
function extractStatus($: cheerio.CheerioAPI): string | null {
|
||||||
const div = $('[data-source="statut"] .pi-data-value');
|
const div = $('[data-source="statut"] .pi-data-value');
|
||||||
if (div.length === 0) return 'Alive';
|
if (div.length === 0) return null;
|
||||||
|
|
||||||
const statusText = div.text().trim().toLowerCase();
|
const statusText = div.text().trim().toLowerCase();
|
||||||
|
|
||||||
@@ -669,6 +669,8 @@ function extractStatus($: cheerio.CheerioAPI): string {
|
|||||||
return 'Alive';
|
return 'Alive';
|
||||||
} else if (statusText.includes('décédé')) {
|
} else if (statusText.includes('décédé')) {
|
||||||
return 'Dead';
|
return 'Dead';
|
||||||
|
} else if (statusText.includes('inconnu')) {
|
||||||
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Alive';
|
return 'Alive';
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { user } from './auth.schema';
|
|||||||
// Define devil fruit types
|
// Define devil fruit types
|
||||||
export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Smile' | 'Unknown';
|
export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Smile' | 'Unknown';
|
||||||
|
|
||||||
|
export type Status = 'Alive' | 'Dead' | 'Unknown';
|
||||||
|
|
||||||
// Define the site config table schema
|
// Define the site config table schema
|
||||||
export const config = sqliteTable('config', {
|
export const config = sqliteTable('config', {
|
||||||
key: text('key').primaryKey(),
|
key: text('key').primaryKey(),
|
||||||
@@ -44,7 +46,7 @@ export const character = sqliteTable('character', {
|
|||||||
firstAppearance: integer('firstAppearance').notNull(),
|
firstAppearance: integer('firstAppearance').notNull(),
|
||||||
pictureUrl: text('pictureUrl'),
|
pictureUrl: text('pictureUrl'),
|
||||||
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
||||||
status: text('status'),
|
status: text('status').$type<Status | null>(),
|
||||||
arcId: text('arcId').references(() => arc.id),
|
arcId: text('arcId').references(() => arc.id),
|
||||||
url: text('url'),
|
url: text('url'),
|
||||||
isInDailyMode: integer('isInDailyMode', { mode: 'boolean' }).default(true)
|
isInDailyMode: integer('isInDailyMode', { mode: 'boolean' }).default(true)
|
||||||
@@ -67,7 +69,7 @@ export const characterOverride = sqliteTable('characterOverride', {
|
|||||||
firstAppearance: integer('firstAppearance'),
|
firstAppearance: integer('firstAppearance'),
|
||||||
pictureUrl: text('pictureUrl'),
|
pictureUrl: text('pictureUrl'),
|
||||||
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
||||||
status: text('status'),
|
status: text('status').$type<Status | null>(),
|
||||||
arcId: text('arcId').references(() => arc.id),
|
arcId: text('arcId').references(() => arc.id),
|
||||||
url: text('url'),
|
url: text('url'),
|
||||||
notes: text('notes')
|
notes: text('notes')
|
||||||
@@ -90,7 +92,7 @@ export const characterScrapeValidation = sqliteTable('characterScrapeValidation'
|
|||||||
firstAppearance: integer('firstAppearance').notNull(),
|
firstAppearance: integer('firstAppearance').notNull(),
|
||||||
pictureUrl: text('pictureUrl'),
|
pictureUrl: text('pictureUrl'),
|
||||||
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
epithets: text('epithets', { mode: 'json' }).$type<string[]>(),
|
||||||
status: text('status'),
|
status: text('status').$type<Status | null>(),
|
||||||
arcId: text('arcId').references(() => arc.id),
|
arcId: text('arcId').references(() => arc.id),
|
||||||
url: text('url')
|
url: text('url')
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,64 @@
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { character, characterScrapeValidation } from '$lib/server/db/schema';
|
import { character, characterScrapeValidation } from '$lib/server/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
async function upsertCharacterFromScrapeValidation(characterId: string): Promise<boolean> {
|
||||||
|
const [scraped] = await db
|
||||||
|
.select()
|
||||||
|
.from(characterScrapeValidation)
|
||||||
|
.where(eq(characterScrapeValidation.id, characterId));
|
||||||
|
|
||||||
|
if (!scraped) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.insert(character)
|
||||||
|
.values({
|
||||||
|
id: scraped.id,
|
||||||
|
name: scraped.name,
|
||||||
|
gender: scraped.gender,
|
||||||
|
age: scraped.age,
|
||||||
|
affiliations: scraped.affiliations,
|
||||||
|
devilFruitId: scraped.devilFruitId,
|
||||||
|
hakiObservation: scraped.hakiObservation,
|
||||||
|
hakiArmament: scraped.hakiArmament,
|
||||||
|
hakiConqueror: scraped.hakiConqueror,
|
||||||
|
bounty: scraped.bounty,
|
||||||
|
height: scraped.height,
|
||||||
|
origin: scraped.origin,
|
||||||
|
firstAppearance: scraped.firstAppearance,
|
||||||
|
pictureUrl: scraped.pictureUrl,
|
||||||
|
epithets: scraped.epithets,
|
||||||
|
status: scraped.status,
|
||||||
|
arcId: scraped.arcId,
|
||||||
|
url: scraped.url
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: character.id,
|
||||||
|
set: {
|
||||||
|
name: scraped.name,
|
||||||
|
gender: scraped.gender,
|
||||||
|
age: scraped.age,
|
||||||
|
affiliations: scraped.affiliations,
|
||||||
|
devilFruitId: scraped.devilFruitId,
|
||||||
|
hakiObservation: scraped.hakiObservation,
|
||||||
|
hakiArmament: scraped.hakiArmament,
|
||||||
|
hakiConqueror: scraped.hakiConqueror,
|
||||||
|
bounty: scraped.bounty,
|
||||||
|
height: scraped.height,
|
||||||
|
origin: scraped.origin,
|
||||||
|
firstAppearance: scraped.firstAppearance,
|
||||||
|
pictureUrl: scraped.pictureUrl,
|
||||||
|
epithets: scraped.epithets,
|
||||||
|
status: scraped.status,
|
||||||
|
arcId: scraped.arcId,
|
||||||
|
url: scraped.url
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export async function load() {
|
export async function load() {
|
||||||
// Get all characters from both tables
|
// Get all characters from both tables
|
||||||
@@ -86,3 +145,37 @@ export async function load() {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
acceptOne: async ({ request }) => {
|
||||||
|
const formData = await request.formData();
|
||||||
|
const characterId = formData.get('characterId');
|
||||||
|
|
||||||
|
if (!characterId || typeof characterId !== 'string') {
|
||||||
|
return { success: false, message: 'characterId is required' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const applied = await upsertCharacterFromScrapeValidation(characterId);
|
||||||
|
return {
|
||||||
|
success: applied,
|
||||||
|
message: applied ? 'Character applied successfully' : 'Character not found in scrape validation table'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
acceptAll: async () => {
|
||||||
|
const scrapedCharacters = await db.select().from(characterScrapeValidation);
|
||||||
|
let appliedCount = 0;
|
||||||
|
|
||||||
|
for (const scraped of scrapedCharacters) {
|
||||||
|
const applied = await upsertCharacterFromScrapeValidation(scraped.id);
|
||||||
|
if (applied) {
|
||||||
|
appliedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
appliedCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -6,6 +6,11 @@
|
|||||||
const newCharacters = $derived(data.changes.filter((c: any) => c.type === 'new'));
|
const newCharacters = $derived(data.changes.filter((c: any) => c.type === 'new'));
|
||||||
const modifiedCharacters = $derived(data.changes.filter((c: any) => c.type === 'modified'));
|
const modifiedCharacters = $derived(data.changes.filter((c: any) => c.type === 'modified'));
|
||||||
|
|
||||||
|
function fandomUrl(path: string | null | undefined): string {
|
||||||
|
if (!path) return 'https://onepiece.fandom.com/fr/wiki';
|
||||||
|
return `https://onepiece.fandom.com/fr/wiki/${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatValue(value: any): string {
|
function formatValue(value: any): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return '—';
|
return '—';
|
||||||
@@ -35,6 +40,16 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 mb-2">Character Changes</h1>
|
<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>
|
<p class="text-gray-400">Total changes: {newCharacters.length} new, {modifiedCharacters.length} modified</p>
|
||||||
|
{#if newCharacters.length + modifiedCharacters.length > 0}
|
||||||
|
<form method="POST" action="?/acceptAll" class="mt-4">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-full border border-emerald-300/40 bg-emerald-500/20 px-4 py-2 text-sm font-semibold text-emerald-100 transition hover:bg-emerald-500/30"
|
||||||
|
>
|
||||||
|
✅ Accepter tous les changements
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- New Characters Section -->
|
<!-- New Characters Section -->
|
||||||
@@ -46,18 +61,31 @@
|
|||||||
<div class="grid gap-4">
|
<div class="grid gap-4">
|
||||||
{#each newCharacters as change (change.id)}
|
{#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="rounded-lg border border-emerald-500/30 bg-emerald-500/5 p-4 space-y-3">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
{#if change.scraped.pictureUrl}
|
<div class="flex items-center gap-3">
|
||||||
<img
|
{#if change.scraped.pictureUrl}
|
||||||
src={change.scraped.pictureUrl}
|
<a href={fandomUrl(change.scraped.url)} target="_blank" rel="noopener noreferrer">
|
||||||
alt={change.scraped.name}
|
<img
|
||||||
class="w-12 h-12 rounded object-cover"
|
src={change.scraped.pictureUrl}
|
||||||
/>
|
alt={change.scraped.name}
|
||||||
{/if}
|
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
|
||||||
<div>
|
/>
|
||||||
<h3 class="font-bold text-emerald-300">{change.scraped.name}</h3>
|
</a>
|
||||||
<p class="text-sm text-gray-500">{change.id}</p>
|
{/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>
|
||||||
|
<form method="POST" action="?/acceptOne">
|
||||||
|
<input type="hidden" name="characterId" value={change.id} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-full border border-emerald-300/40 bg-emerald-500/20 px-3 py-1 text-xs font-semibold text-emerald-100 transition hover:bg-emerald-500/30"
|
||||||
|
>
|
||||||
|
Accepter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||||
<div>
|
<div>
|
||||||
@@ -92,18 +120,31 @@
|
|||||||
<div class="grid gap-6">
|
<div class="grid gap-6">
|
||||||
{#each modifiedCharacters as change (change.id)}
|
{#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="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">
|
<div class="flex items-center justify-between gap-3 pb-4 border-b border-amber-500/20">
|
||||||
{#if change.current?.pictureUrl}
|
<div class="flex items-center gap-3">
|
||||||
<img
|
{#if change.current?.pictureUrl}
|
||||||
src={change.current.pictureUrl}
|
<a href={fandomUrl(change.current?.url ?? change.scraped.url)} target="_blank" rel="noopener noreferrer">
|
||||||
alt={change.current.name}
|
<img
|
||||||
class="w-12 h-12 rounded object-cover"
|
src={change.current.pictureUrl}
|
||||||
/>
|
alt={change.current.name}
|
||||||
{/if}
|
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
|
||||||
<div>
|
/>
|
||||||
<h3 class="font-bold text-amber-300">{change.current?.name ?? change.scraped.name}</h3>
|
</a>
|
||||||
<p class="text-sm text-gray-500">{change.id}</p>
|
{/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>
|
</div>
|
||||||
|
<form method="POST" action="?/acceptOne">
|
||||||
|
<input type="hidden" name="characterId" value={change.id} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-full border border-amber-300/40 bg-amber-500/20 px-3 py-1 text-xs font-semibold text-amber-100 transition hover:bg-amber-500/30"
|
||||||
|
>
|
||||||
|
Accepter
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if change.differences}
|
{#if change.differences}
|
||||||
|
|||||||
Reference in New Issue
Block a user