Compare commits
17 Commits
997b2f1781
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| ef6bf9862e | |||
| d75c74ac3c | |||
| fa14156d82 | |||
| 29297d3773 | |||
| 28bb8f526b | |||
| 288271fb04 | |||
| fb64c84a17 | |||
| 81e205dd4e | |||
| ded1c8313d | |||
| 4426b5d28a | |||
| 5ad0428420 | |||
| 7760570365 | |||
| 5fde54a2a7 | |||
| 2a3c82f777 | |||
| 835163f5bb | |||
| 5020393b22 | |||
| 94393851c8 |
1
drizzle/0001_fuzzy_talisman.sql
Normal file
1
drizzle/0001_fuzzy_talisman.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `character_scrape_validation` ADD `is_deleted` integer DEFAULT false;
|
||||||
1
drizzle/0002_old_earthquake.sql
Normal file
1
drizzle/0002_old_earthquake.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE `character_override`;
|
||||||
8
drizzle/0003_mixed_ben_grimm.sql
Normal file
8
drizzle/0003_mixed_ben_grimm.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE `character` ADD `affiliation` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` ADD `fr_affiliation` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` DROP COLUMN `affiliations`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character` DROP COLUMN `fr_affiliations`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character_scrape_validation` ADD `affiliation` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character_scrape_validation` ADD `fr_affiliation` text;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character_scrape_validation` DROP COLUMN `affiliations`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `character_scrape_validation` DROP COLUMN `fr_affiliations`;
|
||||||
1396
drizzle/meta/0001_snapshot.json
Normal file
1396
drizzle/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
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
1185
drizzle/meta/0003_snapshot.json
Normal file
1185
drizzle/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,27 @@
|
|||||||
"when": 1773602933375,
|
"when": 1773602933375,
|
||||||
"tag": "0000_huge_doctor_octopus",
|
"tag": "0000_huge_doctor_octopus",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1773697753818,
|
||||||
|
"tag": "0001_fuzzy_talisman",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1775950314114,
|
||||||
|
"tag": "0002_old_earthquake",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1776195681488,
|
||||||
|
"tag": "0003_mixed_ben_grimm",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createClient } from '@libsql/client';
|
import { createClient } from '@libsql/client';
|
||||||
import { drizzle } from 'drizzle-orm/libsql';
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
import { sql, eq } from 'drizzle-orm';
|
import { sql, eq, inArray } from 'drizzle-orm';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema';
|
import { arc, character, devilFruit, characterScrapeValidation, type DevilFruitType } from '../src/lib/server/db/schema';
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ type CharacterRecord = {
|
|||||||
frName?: string | null;
|
frName?: string | null;
|
||||||
gender?: string | null;
|
gender?: string | null;
|
||||||
age?: number | null;
|
age?: number | null;
|
||||||
affiliations?: string[] | string | null;
|
affiliation?: string | null;
|
||||||
frAffiliations?: string[] | string | null;
|
frAffiliation?: string | null;
|
||||||
devilFruitId?: string | null;
|
devilFruitId?: string | null;
|
||||||
hakiObservation?: boolean;
|
hakiObservation?: boolean;
|
||||||
hakiArmament?: boolean;
|
hakiArmament?: boolean;
|
||||||
@@ -123,8 +123,8 @@ function transformCharacterData(item: CharacterRecord) {
|
|||||||
frName: toNullable(item.frName),
|
frName: toNullable(item.frName),
|
||||||
gender: toNullable(item.gender),
|
gender: toNullable(item.gender),
|
||||||
age: toNullable(item.age),
|
age: toNullable(item.age),
|
||||||
affiliations: toJsonArray(item.affiliations),
|
affiliation: toNullable(item.affiliation),
|
||||||
frAffiliations: toJsonArray(item.frAffiliations),
|
frAffiliation: toNullable(item.frAffiliation),
|
||||||
devilFruitId: toNullable(item.devilFruitId),
|
devilFruitId: toNullable(item.devilFruitId),
|
||||||
hakiObservation: !!item.hakiObservation,
|
hakiObservation: !!item.hakiObservation,
|
||||||
hakiArmament: !!item.hakiArmament,
|
hakiArmament: !!item.hakiArmament,
|
||||||
@@ -140,7 +140,8 @@ function transformCharacterData(item: CharacterRecord) {
|
|||||||
status: toNullable(item.status),
|
status: toNullable(item.status),
|
||||||
arcId: toNullable(item.arcId),
|
arcId: toNullable(item.arcId),
|
||||||
url: toNullable(item.url),
|
url: toNullable(item.url),
|
||||||
frUrl: toNullable(item.frUrl)
|
frUrl: toNullable(item.frUrl),
|
||||||
|
isDeleted: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -307,6 +308,7 @@ async function importFromJson(): Promise<void> {
|
|||||||
} else {
|
} else {
|
||||||
// Update scrapeValidation table
|
// Update scrapeValidation table
|
||||||
console.log('Characters table not empty, updating scrapeValidation table for changes...\n');
|
console.log('Characters table not empty, updating scrapeValidation table for changes...\n');
|
||||||
|
const scrapedCharacterIds: string[] = [];
|
||||||
|
|
||||||
for (let i = 0; i < characters.length; i++) {
|
for (let i = 0; i < characters.length; i++) {
|
||||||
const item = characters[i];
|
const item = characters[i];
|
||||||
@@ -319,6 +321,7 @@ async function importFromJson(): Promise<void> {
|
|||||||
|
|
||||||
lastSql = selectQuery.toSQL();
|
lastSql = selectQuery.toSQL();
|
||||||
|
|
||||||
|
scrapedCharacterIds.push(item.id);
|
||||||
const jsonData = transformCharacterData(item);
|
const jsonData = transformCharacterData(item);
|
||||||
|
|
||||||
const upsertQuery = db
|
const upsertQuery = db
|
||||||
@@ -341,6 +344,57 @@ async function importFromJson(): Promise<void> {
|
|||||||
logSqlOnError(lastSql);
|
logSqlOnError(lastSql);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch all characters from the character table and mark those absent from the
|
||||||
|
// scrape as deleted in scrape validation.
|
||||||
|
const allCharacters = await db.select({ id: character.id }).from(character);
|
||||||
|
const scrapedSet = new Set(scrapedCharacterIds);
|
||||||
|
const idsToMarkDeleted = allCharacters
|
||||||
|
.map((c) => c.id)
|
||||||
|
.filter((id) => !scrapedSet.has(id));
|
||||||
|
|
||||||
|
if (idsToMarkDeleted.length > 0) {
|
||||||
|
console.log(`\n⚠️ Marking ${idsToMarkDeleted.length} character(s) as deleted in scrape validation...`);
|
||||||
|
const deletedCharacterRows = await db
|
||||||
|
.select()
|
||||||
|
.from(character)
|
||||||
|
.where(inArray(character.id, idsToMarkDeleted));
|
||||||
|
|
||||||
|
for (const row of deletedCharacterRows) {
|
||||||
|
await db
|
||||||
|
.insert(characterScrapeValidation)
|
||||||
|
.values({
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
frName: row.frName,
|
||||||
|
gender: row.gender,
|
||||||
|
age: row.age,
|
||||||
|
affiliation: row.affiliation,
|
||||||
|
frAffiliation: row.frAffiliation,
|
||||||
|
devilFruitId: row.devilFruitId,
|
||||||
|
hakiObservation: row.hakiObservation,
|
||||||
|
hakiArmament: row.hakiArmament,
|
||||||
|
hakiConqueror: row.hakiConqueror,
|
||||||
|
bounty: row.bounty,
|
||||||
|
height: row.height,
|
||||||
|
origin: row.origin,
|
||||||
|
frOrigin: row.frOrigin,
|
||||||
|
firstAppearance: row.firstAppearance,
|
||||||
|
pictureUrl: row.pictureUrl,
|
||||||
|
epithets: row.epithets,
|
||||||
|
frEpithets: row.frEpithets,
|
||||||
|
status: row.status,
|
||||||
|
arcId: row.arcId,
|
||||||
|
url: row.url,
|
||||||
|
frUrl: row.frUrl,
|
||||||
|
isDeleted: true
|
||||||
|
})
|
||||||
|
.onConflictDoUpdate({
|
||||||
|
target: characterScrapeValidation.id,
|
||||||
|
set: { isDeleted: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n\n✓ Characters imported!`);
|
console.log(`\n\n✓ Characters imported!`);
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ const columns = [
|
|||||||
'origin',
|
'origin',
|
||||||
'devilFruitType',
|
'devilFruitType',
|
||||||
'arc',
|
'arc',
|
||||||
'status'
|
'status',
|
||||||
|
'age'
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
async function initColumnConfig(): Promise<void> {
|
async function initColumnConfig(): Promise<void> {
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ interface Character {
|
|||||||
frOrigin: string | null;
|
frOrigin: string | null;
|
||||||
devilFruitId: string | null;
|
devilFruitId: string | null;
|
||||||
devilFruitUrl: string | null;
|
devilFruitUrl: string | null;
|
||||||
affiliations: string[];
|
affiliation: string | null;
|
||||||
frAffiliations: string[] | null;
|
frAffiliation: string | null;
|
||||||
bounty: number | null;
|
bounty: number | null;
|
||||||
hakiObservation: boolean;
|
hakiObservation: boolean;
|
||||||
hakiArmament: boolean;
|
hakiArmament: boolean;
|
||||||
@@ -307,6 +307,10 @@ async function fetchAllCharacters(arcsList: Arc[]): Promise<Character[]> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (charName.toLowerCase().includes('family') || charName === 'Four Emperors') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (charUrl) {
|
if (charUrl) {
|
||||||
charUrl = charUrl.replace('/wiki/', '');
|
charUrl = charUrl.replace('/wiki/', '');
|
||||||
characterList.push({
|
characterList.push({
|
||||||
@@ -366,7 +370,7 @@ async function fetchAllCharacters(arcsList: Arc[]): Promise<Character[]> {
|
|||||||
Age: data.age,
|
Age: data.age,
|
||||||
Status: data.status,
|
Status: data.status,
|
||||||
Epithets: data.epithets.join(', '),
|
Epithets: data.epithets.join(', '),
|
||||||
Affiliations: data.affiliations.join(', '),
|
Affiliation: data.affiliation,
|
||||||
DevilFruitId: data.devilFruitId,
|
DevilFruitId: data.devilFruitId,
|
||||||
DevilFruitUrl: data.devilFruitUrl,
|
DevilFruitUrl: data.devilFruitUrl,
|
||||||
HakiObservation: data.hakiObservation ? 'Yes' : 'No',
|
HakiObservation: data.hakiObservation ? 'Yes' : 'No',
|
||||||
@@ -437,10 +441,10 @@ async function fetchCharacter(
|
|||||||
let gender: string | null = null;
|
let gender: string | null = null;
|
||||||
for (const cat of categories) {
|
for (const cat of categories) {
|
||||||
const catName = cat['*'] || '';
|
const catName = cat['*'] || '';
|
||||||
if (catName === 'Male_Characters') {
|
if (catName === 'Male_Characters' || catName === 'Kings' || catName === 'Princes' || catName === 'Former_Kings' || catName === 'Former_Princes') {
|
||||||
gender = 'Male';
|
gender = 'Male';
|
||||||
break;
|
break;
|
||||||
} else if (catName === 'Female_Characters') {
|
} else if (catName === 'Female_Characters' || catName === 'Queens' || catName === 'Princesses' || catName === 'Former_Queens' || catName === 'Former_Princesses' || catName === 'Queens_Regnant' || catName === 'Gorgon_Sisters' || catName === 'Kuja') {
|
||||||
gender = 'Female';
|
gender = 'Female';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -449,8 +453,8 @@ async function fetchCharacter(
|
|||||||
// Extract age
|
// Extract age
|
||||||
const age = extractAge($);
|
const age = extractAge($);
|
||||||
|
|
||||||
// Extract affiliations
|
// Extract affiliation
|
||||||
const affiliations = await extractAffiliations($, 'en');
|
const affiliation = await extractAffiliations($, 'en');
|
||||||
|
|
||||||
// Extract epithets
|
// Extract epithets
|
||||||
const epithets = extractEpithets($);
|
const epithets = extractEpithets($);
|
||||||
@@ -509,7 +513,7 @@ async function fetchCharacter(
|
|||||||
|
|
||||||
let frName = frjsonData?.parse?.title || null;
|
let frName = frjsonData?.parse?.title || null;
|
||||||
|
|
||||||
const frAffiliations = frjsonData
|
const frAffiliation = frjsonData
|
||||||
? await extractAffiliations(cheerio.load(frjsonData.parse?.text?.['*'] || ''), 'fr')
|
? await extractAffiliations(cheerio.load(frjsonData.parse?.text?.['*'] || ''), 'fr')
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -538,8 +542,8 @@ async function fetchCharacter(
|
|||||||
frOrigin,
|
frOrigin,
|
||||||
devilFruitId,
|
devilFruitId,
|
||||||
devilFruitUrl,
|
devilFruitUrl,
|
||||||
affiliations,
|
affiliation,
|
||||||
frAffiliations,
|
frAffiliation,
|
||||||
bounty,
|
bounty,
|
||||||
hakiObservation,
|
hakiObservation,
|
||||||
hakiArmament,
|
hakiArmament,
|
||||||
@@ -587,15 +591,15 @@ function extractAge($: cheerio.CheerioAPI): number | null {
|
|||||||
/**
|
/**
|
||||||
* Extract affiliations from infobox
|
* Extract affiliations from infobox
|
||||||
*/
|
*/
|
||||||
async function extractAffiliations($: cheerio.CheerioAPI, lang: string): Promise<string[]> {
|
async function extractAffiliations($: cheerio.CheerioAPI, lang: string): Promise<string | null> {
|
||||||
const div = $('[data-source="affiliation"] .pi-data-value');
|
const div = $('[data-source="affiliation"] .pi-data-value');
|
||||||
if (div.length === 0) return [];
|
if (div.length === 0) return null;
|
||||||
|
|
||||||
const cleanedDiv = div.clone();
|
const cleanedDiv = div.clone();
|
||||||
cleanedDiv.find('sup').remove();
|
cleanedDiv.find('sup').remove();
|
||||||
|
|
||||||
const text = cleanedDiv.html();
|
const text = cleanedDiv.html();
|
||||||
if (!text) return [];
|
if (!text) return null;
|
||||||
|
|
||||||
// Resolve affiliations from linked page titles.
|
// Resolve affiliations from linked page titles.
|
||||||
const links = cleanedDiv.find('a').toArray();
|
const links = cleanedDiv.find('a').toArray();
|
||||||
@@ -620,14 +624,14 @@ async function extractAffiliations($: cheerio.CheerioAPI, lang: string): Promise
|
|||||||
|
|
||||||
const uniqueLinks = Array.from(new Set(linkValues.filter(Boolean)));
|
const uniqueLinks = Array.from(new Set(linkValues.filter(Boolean)));
|
||||||
if (uniqueLinks.length > 0) {
|
if (uniqueLinks.length > 0) {
|
||||||
return uniqueLinks;
|
return uniqueLinks[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to parsing text
|
// Fallback to parsing text
|
||||||
const cleanText = text.replace(/<[^>]*>/g, '').trim();
|
const cleanText = text.replace(/<[^>]*>/g, '').trim();
|
||||||
const parts = cleanText.split(/\s*\n\s*|\s*;\s*|\s*,\s*/).filter(Boolean);
|
const parts = cleanText.split(/\s*\n\s*|\s*;\s*|\s*,\s*/).filter(Boolean);
|
||||||
return parts.length > 0 ? parts : [];
|
return parts.length > 0 ? parts[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -635,7 +639,9 @@ async function extractAffiliations($: cheerio.CheerioAPI, lang: string): Promise
|
|||||||
* Handles both quoted and unquoted epithets, keeping only the main/latest readable values.
|
* Handles both quoted and unquoted epithets, keeping only the main/latest readable values.
|
||||||
*/
|
*/
|
||||||
function extractEpithets($: cheerio.CheerioAPI): string[] {
|
function extractEpithets($: cheerio.CheerioAPI): string[] {
|
||||||
const div = $('[data-source="epithet"] .pi-data-value');
|
const div = $(
|
||||||
|
'[data-source="epithet"] .pi-data-value, [data-source="épithète"] .pi-data-value'
|
||||||
|
).first();
|
||||||
if (div.length === 0) return [];
|
if (div.length === 0) return [];
|
||||||
|
|
||||||
const cleanedDiv = div.clone();
|
const cleanedDiv = div.clone();
|
||||||
@@ -802,11 +808,11 @@ function extractStatus($: cheerio.CheerioAPI): string | null {
|
|||||||
|
|
||||||
const statusText = div.text().trim().toLowerCase();
|
const statusText = div.text().trim().toLowerCase();
|
||||||
|
|
||||||
if (statusText.includes('Alive')) {
|
if (statusText.includes('alive')) {
|
||||||
return 'Alive';
|
return 'Alive';
|
||||||
} else if (statusText.includes('Dead')) {
|
} else if (statusText.includes('deceased')) {
|
||||||
return 'Dead';
|
return 'Dead';
|
||||||
} else if (statusText.includes('Unknown')) {
|
} else if (statusText.includes('unknown')) {
|
||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -863,9 +869,8 @@ async function saveToCSV(characters: Character[]): Promise<void> {
|
|||||||
status: c.status || '',
|
status: c.status || '',
|
||||||
epithets: Array.isArray(c.epithets) ? c.epithets.join(', ') : c.epithets || '',
|
epithets: Array.isArray(c.epithets) ? c.epithets.join(', ') : c.epithets || '',
|
||||||
devilFruitId: c.devilFruitId || '',
|
devilFruitId: c.devilFruitId || '',
|
||||||
affiliations: Array.isArray(c.affiliations)
|
affiliation: c.affiliation || '',
|
||||||
? c.affiliations.join(', ')
|
frAffiliation: c.frAffiliation || '',
|
||||||
: c.affiliations || '',
|
|
||||||
bounty: c.bounty ?? 0,
|
bounty: c.bounty ?? 0,
|
||||||
hakiObservation: c.hakiObservation ? 1 : 0,
|
hakiObservation: c.hakiObservation ? 1 : 0,
|
||||||
hakiArmament: c.hakiArmament ? 1 : 0,
|
hakiArmament: c.hakiArmament ? 1 : 0,
|
||||||
|
|||||||
73
src/lib/components/FriendsTodaySection.svelte
Normal file
73
src/lib/components/FriendsTodaySection.svelte
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { t } from '$lib/i18n';
|
||||||
|
|
||||||
|
type TriedCharacter = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
pictureUrl: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
type FriendTodayResult = {
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
image: string | null;
|
||||||
|
tryCount: number;
|
||||||
|
triedCharacters: TriedCharacter[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export let friendsTodayResults: FriendTodayResult[] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if friendsTodayResults.length > 0}
|
||||||
|
<section class="mt-6 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">{$t.game.daily.friendsToday}</p>
|
||||||
|
<div class="mt-4 space-y-2">
|
||||||
|
{#each friendsTodayResults as friendResult (friendResult.userId)}
|
||||||
|
<div class="rounded-lg border border-white/10 bg-slate-950/50 px-4 py-3">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{#if friendResult.image}
|
||||||
|
<img
|
||||||
|
src={friendResult.image}
|
||||||
|
alt={friendResult.name}
|
||||||
|
class="h-8 w-8 rounded-full border border-white/20 object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-amber-300/20 text-xs font-semibold text-amber-100">
|
||||||
|
{friendResult.name?.charAt(0).toUpperCase() || 'U'}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<p class="text-sm font-semibold text-slate-100">{friendResult.name}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-amber-300">
|
||||||
|
{friendResult.tryCount} {friendResult.tryCount > 1 ? $t.game.daily.friendTryPlural : $t.game.daily.friendTrySingular}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 border-t border-white/10 pt-2">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||||
|
{$t.game.daily.friendsTriedCharacters}
|
||||||
|
</p>
|
||||||
|
{#if friendResult.triedCharacters && friendResult.triedCharacters.length > 0}
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
{#each friendResult.triedCharacters as triedCharacter (triedCharacter.id)}
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-2.5 py-1 text-xs text-slate-200">
|
||||||
|
{#if triedCharacter.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={triedCharacter.pictureUrl}
|
||||||
|
alt={triedCharacter.name}
|
||||||
|
class="h-4 w-4 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{triedCharacter.name}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{$t.game.daily.friendsNoTriedCharacters}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { formatBounty } from '$lib';
|
import { formatBounty } from '$lib';
|
||||||
import { resolve } from '$app/paths';
|
|
||||||
import type { CharacterWithRelations } from '$lib/server/daily-character';
|
import type { CharacterWithRelations } from '$lib/server/daily-character';
|
||||||
import { language, t } from '$lib/i18n';
|
import { language, t } from '$lib/i18n';
|
||||||
|
|
||||||
@@ -9,61 +8,16 @@
|
|||||||
export let columnVisibility: {
|
export let columnVisibility: {
|
||||||
status?: boolean;
|
status?: boolean;
|
||||||
gender?: boolean;
|
gender?: boolean;
|
||||||
affiliations?: boolean;
|
affiliation?: boolean;
|
||||||
devilFruitType?: boolean;
|
devilFruitType?: boolean;
|
||||||
haki?: boolean;
|
haki?: boolean;
|
||||||
bounty?: boolean;
|
bounty?: boolean;
|
||||||
height?: boolean;
|
height?: boolean;
|
||||||
|
age?: boolean;
|
||||||
origin?: boolean;
|
origin?: boolean;
|
||||||
arc?: boolean;
|
arc?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function normalizeAffiliations(value: unknown): string[] {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value
|
|
||||||
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
||||||
.filter((entry) => entry.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
const trimmed = value.trim();
|
|
||||||
if (trimmed.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trimmed.startsWith('[')) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(trimmed);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return parsed
|
|
||||||
.map((entry) => (typeof entry === 'string' ? entry.trim() : ''))
|
|
||||||
.filter((entry) => entry.length > 0);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return trimmed
|
|
||||||
.split(',')
|
|
||||||
.map((entry) => entry.trim())
|
|
||||||
.filter((entry) => entry.length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function firstAffiliation(value: unknown): string | null {
|
|
||||||
const affiliations = normalizeAffiliations(value);
|
|
||||||
return affiliations.length > 0 ? affiliations[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasMatchingPrimaryAffiliation(characterAffiliations: unknown, dailyAffiliations: unknown): boolean {
|
|
||||||
const characterPrimary = firstAffiliation(characterAffiliations);
|
|
||||||
const dailyPrimary = firstAffiliation(dailyAffiliations);
|
|
||||||
return characterPrimary === dailyPrimary;
|
|
||||||
}
|
|
||||||
|
|
||||||
$: isFrench = $language === 'fr';
|
$: isFrench = $language === 'fr';
|
||||||
|
|
||||||
function getDisplayName(character: CharacterWithRelations): string {
|
function getDisplayName(character: CharacterWithRelations): string {
|
||||||
@@ -106,6 +60,14 @@
|
|||||||
return character.arcName;
|
return character.arcName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDislayAffiliation(character: CharacterWithRelations): string | null {
|
||||||
|
if (isFrench && typeof character.frAffiliation === 'string' && character.frAffiliation.length > 0) {
|
||||||
|
return character.frAffiliation;
|
||||||
|
}
|
||||||
|
|
||||||
|
return character.affiliation;
|
||||||
|
}
|
||||||
|
|
||||||
function hasMatchingArc(characterEntry: CharacterWithRelations, dailyEntry: CharacterWithRelations): boolean {
|
function hasMatchingArc(characterEntry: CharacterWithRelations, dailyEntry: CharacterWithRelations): boolean {
|
||||||
return getDisplayArcName(characterEntry) === getDisplayArcName(dailyEntry);
|
return getDisplayArcName(characterEntry) === getDisplayArcName(dailyEntry);
|
||||||
}
|
}
|
||||||
@@ -156,7 +118,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if columnVisibility.affiliations !== false}
|
{#if columnVisibility.affiliation !== false}
|
||||||
<div
|
<div
|
||||||
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24"
|
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24"
|
||||||
>
|
>
|
||||||
@@ -211,6 +173,17 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if columnVisibility.age !== false}
|
||||||
|
<div
|
||||||
|
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="text-[9px] font-semibold tracking-wider text-amber-100 uppercase sm:text-xs"
|
||||||
|
>
|
||||||
|
{$t.game.components.guessHistory.age}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{#if columnVisibility.origin !== false}
|
{#if columnVisibility.origin !== false}
|
||||||
<div
|
<div
|
||||||
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24"
|
class="flex w-16 shrink-0 items-center justify-center rounded-lg border border-amber-200/30 bg-amber-900/30 p-1 text-center sm:w-20 sm:p-2 md:w-24"
|
||||||
@@ -308,23 +281,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Affiliations -->
|
<!-- Affiliations -->
|
||||||
{#if columnVisibility.affiliations !== false}
|
{#if columnVisibility.affiliation !== false}
|
||||||
<div
|
<div
|
||||||
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {hasMatchingPrimaryAffiliation(character.affiliations, dailyCharacter.affiliations)
|
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {getDislayAffiliation(character) === getDislayAffiliation(dailyCharacter)
|
||||||
? 'bg-emerald-600/90'
|
? 'bg-emerald-600/90'
|
||||||
: 'bg-red-900/60'} flex items-center justify-center overflow-hidden p-1 sm:p-2"
|
: 'bg-red-900/60'} flex items-center justify-center p-1 sm:p-2"
|
||||||
>
|
>
|
||||||
{#if firstAffiliation(character.affiliations)}
|
<p class="text-center text-[10px] font-bold text-white sm:text-xs md:text-sm">
|
||||||
<p
|
{getDislayAffiliation(character) || $t.game.components.guessHistory.unknown}
|
||||||
class="w-full text-center text-[10px] leading-tight font-bold wrap-break-word whitespace-normal text-white sm:text-xs md:text-sm"
|
</p>
|
||||||
>
|
|
||||||
{firstAffiliation(character.affiliations)}
|
|
||||||
</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-center text-xs font-bold text-slate-400 sm:text-sm md:text-base">
|
|
||||||
-
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -451,6 +416,41 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Age -->
|
||||||
|
{#if columnVisibility.age !== false}
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 shrink-0 rounded-lg border border-white/10 sm:h-20 sm:w-20 md:h-24 md:w-24 {character.age ===
|
||||||
|
dailyCharacter.age
|
||||||
|
? 'bg-emerald-600/90'
|
||||||
|
: 'bg-red-900/60'} relative flex items-center justify-center overflow-hidden p-1 sm:p-2"
|
||||||
|
>
|
||||||
|
{#if character.age != null && dailyCharacter.age != null && character.age !== dailyCharacter.age}
|
||||||
|
<div
|
||||||
|
class="pointer-events-none absolute h-full w-full opacity-30"
|
||||||
|
style="
|
||||||
|
background-color: rgb(203, 213, 225);
|
||||||
|
clip-path: {character.age > dailyCharacter.age
|
||||||
|
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
||||||
|
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
{#if character.age != null}
|
||||||
|
<p
|
||||||
|
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
|
||||||
|
>
|
||||||
|
{character.age}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p
|
||||||
|
class="relative z-10 text-center text-[10px] font-bold text-white sm:text-xs md:text-sm"
|
||||||
|
>
|
||||||
|
{$t.game.components.guessHistory.unknown}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Origine -->
|
<!-- Origine -->
|
||||||
{#if columnVisibility.origin !== false}
|
{#if columnVisibility.origin !== false}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -86,10 +86,7 @@
|
|||||||
>
|
>
|
||||||
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.affiliation}</p>
|
<p class="text-sm font-medium text-amber-100">{$t.game.components.hints.affiliation}</p>
|
||||||
{#if showHintAffiliation}
|
{#if showHintAffiliation}
|
||||||
{@const affiliations = typeof dailyCharacter.affiliations === 'string'
|
<p class="mt-2 text-xs text-white font-semibold">{isFrench && dailyCharacter.frAffiliation ? dailyCharacter.frAffiliation : dailyCharacter.affiliation || $t.game.components.hints.unknown}</p>
|
||||||
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
|
|
||||||
: dailyCharacter.affiliations}
|
|
||||||
<p class="mt-2 text-xs text-white font-semibold">{Array.isArray(affiliations) ? affiliations[0] : affiliations || $t.game.components.hints.unknown}</p>
|
|
||||||
{:else if Math.max(0, 15 - selectedCharacters.length) > 0}
|
{:else if Math.max(0, 15 - selectedCharacters.length) > 0}
|
||||||
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} {$t.game.components.hints.beforeUnlock}</p>
|
||||||
{:else}
|
{:else}
|
||||||
|
|||||||
@@ -88,6 +88,8 @@
|
|||||||
"changePassword": "Change password",
|
"changePassword": "Change password",
|
||||||
"dailyHistoryTitle": "Daily history",
|
"dailyHistoryTitle": "Daily history",
|
||||||
"noDailyHistory": "No history available",
|
"noDailyHistory": "No history available",
|
||||||
|
"triedCharactersTitle": "Tried characters",
|
||||||
|
"noTriedCharacters": "No characters recorded",
|
||||||
"noImage": "N/A",
|
"noImage": "N/A",
|
||||||
"trySingular": "try",
|
"trySingular": "try",
|
||||||
"tryPlural": "tries",
|
"tryPlural": "tries",
|
||||||
@@ -111,6 +113,8 @@
|
|||||||
"reset": "Play again",
|
"reset": "Play again",
|
||||||
"description": "Guess the character. Each hint unlocks after a certain number of guesses. Good luck!",
|
"description": "Guess the character. Each hint unlocks after a certain number of guesses. Good luck!",
|
||||||
"friendsToday": "Your friends today",
|
"friendsToday": "Your friends today",
|
||||||
|
"friendsTriedCharacters": "Tried characters",
|
||||||
|
"friendsNoTriedCharacters": "No characters recorded",
|
||||||
"friendTrySingular": "try",
|
"friendTrySingular": "try",
|
||||||
"friendTryPlural": "tries"
|
"friendTryPlural": "tries"
|
||||||
},
|
},
|
||||||
@@ -140,6 +144,7 @@
|
|||||||
"withFruit": "With Fruit",
|
"withFruit": "With Fruit",
|
||||||
"withoutFruit": "Without Fruit",
|
"withoutFruit": "Without Fruit",
|
||||||
"heightDefined": "Height defined",
|
"heightDefined": "Height defined",
|
||||||
|
"ageDefined": "Age defined",
|
||||||
"originDefined": "Origin defined",
|
"originDefined": "Origin defined",
|
||||||
"availableCharactersSingular": "character available",
|
"availableCharactersSingular": "character available",
|
||||||
"availableCharactersPlural": "characters available",
|
"availableCharactersPlural": "characters available",
|
||||||
@@ -171,6 +176,7 @@
|
|||||||
"haki": "Haki",
|
"haki": "Haki",
|
||||||
"bounty": "Bounty",
|
"bounty": "Bounty",
|
||||||
"height": "Height",
|
"height": "Height",
|
||||||
|
"age": "Age",
|
||||||
"origin": "Origin",
|
"origin": "Origin",
|
||||||
"arc": "Arc",
|
"arc": "Arc",
|
||||||
"alive": "Alive",
|
"alive": "Alive",
|
||||||
|
|||||||
@@ -88,6 +88,8 @@
|
|||||||
"changePassword": "Changer le mot de passe",
|
"changePassword": "Changer le mot de passe",
|
||||||
"dailyHistoryTitle": "Historique des Daily",
|
"dailyHistoryTitle": "Historique des Daily",
|
||||||
"noDailyHistory": "Aucun historique disponible",
|
"noDailyHistory": "Aucun historique disponible",
|
||||||
|
"triedCharactersTitle": "Personnages essayes",
|
||||||
|
"noTriedCharacters": "Aucun personnage enregistre",
|
||||||
"noImage": "N/A",
|
"noImage": "N/A",
|
||||||
"trySingular": "tentative",
|
"trySingular": "tentative",
|
||||||
"tryPlural": "tentatives",
|
"tryPlural": "tentatives",
|
||||||
@@ -111,6 +113,8 @@
|
|||||||
"reset": "Recommencer",
|
"reset": "Recommencer",
|
||||||
"description": "Devine le personnage. Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
|
"description": "Devine le personnage. Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
|
||||||
"friendsToday": "Tes amis aujourd'hui",
|
"friendsToday": "Tes amis aujourd'hui",
|
||||||
|
"friendsTriedCharacters": "Personnages essayes",
|
||||||
|
"friendsNoTriedCharacters": "Aucun personnage enregistre",
|
||||||
"friendTrySingular": "coup",
|
"friendTrySingular": "coup",
|
||||||
"friendTryPlural": "coups"
|
"friendTryPlural": "coups"
|
||||||
},
|
},
|
||||||
@@ -140,6 +144,7 @@
|
|||||||
"withFruit": "Avec Fruit",
|
"withFruit": "Avec Fruit",
|
||||||
"withoutFruit": "Sans Fruit",
|
"withoutFruit": "Sans Fruit",
|
||||||
"heightDefined": "Taille definie",
|
"heightDefined": "Taille definie",
|
||||||
|
"ageDefined": "Age defini",
|
||||||
"originDefined": "Origine definie",
|
"originDefined": "Origine definie",
|
||||||
"availableCharactersSingular": "personnage disponible",
|
"availableCharactersSingular": "personnage disponible",
|
||||||
"availableCharactersPlural": "personnages disponibles",
|
"availableCharactersPlural": "personnages disponibles",
|
||||||
@@ -171,6 +176,7 @@
|
|||||||
"haki": "Haki",
|
"haki": "Haki",
|
||||||
"bounty": "Prime",
|
"bounty": "Prime",
|
||||||
"height": "Taille",
|
"height": "Taille",
|
||||||
|
"age": "Age",
|
||||||
"origin": "Origine",
|
"origin": "Origine",
|
||||||
"arc": "Arc",
|
"arc": "Arc",
|
||||||
"alive": "Vivant",
|
"alive": "Vivant",
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -11,8 +11,8 @@ const characterWithRelationsSelect = {
|
|||||||
frName: character.frName,
|
frName: character.frName,
|
||||||
gender: character.gender,
|
gender: character.gender,
|
||||||
age: character.age,
|
age: character.age,
|
||||||
affiliations: character.affiliations,
|
affiliation: character.affiliation,
|
||||||
frAffiliations: character.frAffiliations,
|
frAffiliation: character.frAffiliation,
|
||||||
devilFruitId: character.devilFruitId,
|
devilFruitId: character.devilFruitId,
|
||||||
devilFruitName: devilFruit.name,
|
devilFruitName: devilFruit.name,
|
||||||
devilFruitType: devilFruit.type,
|
devilFruitType: devilFruit.type,
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ export const character = sqliteTable('character', {
|
|||||||
frName: text('fr_name'),
|
frName: text('fr_name'),
|
||||||
gender: text('gender'),
|
gender: text('gender'),
|
||||||
age: integer('age'),
|
age: integer('age'),
|
||||||
affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
|
affiliation: text('affiliation'),
|
||||||
frAffiliations: text('fr_affiliations', { mode: 'json' }).$type<string[]>(),
|
frAffiliation: text('fr_affiliation'),
|
||||||
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id),
|
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id),
|
||||||
hakiObservation: integer('haki_observation', { mode: 'boolean' }).default(false),
|
hakiObservation: integer('haki_observation', { mode: 'boolean' }).default(false),
|
||||||
hakiArmament: integer('haki_armament', { mode: 'boolean' }).default(false),
|
hakiArmament: integer('haki_armament', { mode: 'boolean' }).default(false),
|
||||||
@@ -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(),
|
||||||
@@ -102,8 +73,8 @@ export const characterScrapeValidation = sqliteTable('character_scrape_validatio
|
|||||||
frName: text('fr_name'),
|
frName: text('fr_name'),
|
||||||
gender: text('gender'),
|
gender: text('gender'),
|
||||||
age: integer('age'),
|
age: integer('age'),
|
||||||
affiliations: text('affiliations', { mode: 'json' }).$type<string[]>(),
|
affiliation: text('affiliation'),
|
||||||
frAffiliations: text('fr_affiliations', { mode: 'json' }).$type<string[]>(),
|
frAffiliation: text('fr_affiliation'),
|
||||||
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id, { onDelete: 'set null' }),
|
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id, { onDelete: 'set null' }),
|
||||||
hakiObservation: integer('haki_observation', { mode: 'boolean' }).default(false),
|
hakiObservation: integer('haki_observation', { mode: 'boolean' }).default(false),
|
||||||
hakiArmament: integer('haki_armament', { mode: 'boolean' }).default(false),
|
hakiArmament: integer('haki_armament', { mode: 'boolean' }).default(false),
|
||||||
@@ -119,7 +90,8 @@ export const characterScrapeValidation = sqliteTable('character_scrape_validatio
|
|||||||
status: text('status').$type<Status | null>(),
|
status: text('status').$type<Status | null>(),
|
||||||
arcId: text('arc_id').references(() => arc.id, { onDelete: 'set null' }),
|
arcId: text('arc_id').references(() => arc.id, { onDelete: 'set null' }),
|
||||||
url: text('url'),
|
url: text('url'),
|
||||||
frUrl: text('fr_url')
|
frUrl: text('fr_url'),
|
||||||
|
isDeleted: integer('is_deleted', { mode: 'boolean' }).default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CharacterScrapeValidation = InferSelectModel<typeof characterScrapeValidation>;
|
export type CharacterScrapeValidation = InferSelectModel<typeof characterScrapeValidation>;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const EXEC_OPTIONS = {
|
|||||||
maxBuffer: 50 * 1024 * 1024
|
maxBuffer: 50 * 1024 * 1024
|
||||||
};
|
};
|
||||||
|
|
||||||
async function upsertCharacterFromScrapeValidation(characterId: string): Promise<boolean> {
|
async function applyCharacterChangeFromScrapeValidation(characterId: string): Promise<boolean> {
|
||||||
const [scraped] = await db
|
const [scraped] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(characterScrapeValidation)
|
.from(characterScrapeValidation)
|
||||||
@@ -21,6 +21,11 @@ async function upsertCharacterFromScrapeValidation(characterId: string): Promise
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scraped.isDeleted) {
|
||||||
|
await db.delete(character).where(eq(character.id, characterId));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.insert(character)
|
.insert(character)
|
||||||
.values({
|
.values({
|
||||||
@@ -29,7 +34,8 @@ async function upsertCharacterFromScrapeValidation(characterId: string): Promise
|
|||||||
frName: scraped.frName,
|
frName: scraped.frName,
|
||||||
gender: scraped.gender,
|
gender: scraped.gender,
|
||||||
age: scraped.age,
|
age: scraped.age,
|
||||||
affiliations: scraped.affiliations,
|
affiliation: scraped.affiliation,
|
||||||
|
frAffiliation: scraped.frAffiliation,
|
||||||
devilFruitId: scraped.devilFruitId,
|
devilFruitId: scraped.devilFruitId,
|
||||||
hakiObservation: scraped.hakiObservation,
|
hakiObservation: scraped.hakiObservation,
|
||||||
hakiArmament: scraped.hakiArmament,
|
hakiArmament: scraped.hakiArmament,
|
||||||
@@ -54,7 +60,8 @@ async function upsertCharacterFromScrapeValidation(characterId: string): Promise
|
|||||||
frName: scraped.frName,
|
frName: scraped.frName,
|
||||||
gender: scraped.gender,
|
gender: scraped.gender,
|
||||||
age: scraped.age,
|
age: scraped.age,
|
||||||
affiliations: scraped.affiliations,
|
affiliation: scraped.affiliation,
|
||||||
|
frAffiliation: scraped.frAffiliation,
|
||||||
devilFruitId: scraped.devilFruitId,
|
devilFruitId: scraped.devilFruitId,
|
||||||
hakiObservation: scraped.hakiObservation,
|
hakiObservation: scraped.hakiObservation,
|
||||||
hakiArmament: scraped.hakiArmament,
|
hakiArmament: scraped.hakiArmament,
|
||||||
@@ -87,7 +94,7 @@ export async function load() {
|
|||||||
|
|
||||||
// Compare and categorize changes
|
// Compare and categorize changes
|
||||||
const changes: {
|
const changes: {
|
||||||
type: 'new' | 'modified';
|
type: 'new' | 'modified' | 'deleted';
|
||||||
id: string;
|
id: string;
|
||||||
scraped: (typeof scrapedCharacters)[0];
|
scraped: (typeof scrapedCharacters)[0];
|
||||||
current?: (typeof currentCharacters)[0];
|
current?: (typeof currentCharacters)[0];
|
||||||
@@ -97,6 +104,18 @@ export async function load() {
|
|||||||
for (const scraped of scrapedCharacters) {
|
for (const scraped of scrapedCharacters) {
|
||||||
const current = currentCharMap.get(scraped.id);
|
const current = currentCharMap.get(scraped.id);
|
||||||
|
|
||||||
|
if (scraped.isDeleted) {
|
||||||
|
if (current) {
|
||||||
|
changes.push({
|
||||||
|
type: 'deleted',
|
||||||
|
id: scraped.id,
|
||||||
|
scraped,
|
||||||
|
current
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (!current) {
|
if (!current) {
|
||||||
// New character
|
// New character
|
||||||
changes.push({
|
changes.push({
|
||||||
@@ -112,7 +131,8 @@ export async function load() {
|
|||||||
'frName',
|
'frName',
|
||||||
'gender',
|
'gender',
|
||||||
'age',
|
'age',
|
||||||
'affiliations',
|
'affiliation',
|
||||||
|
'frAffiliation',
|
||||||
'devilFruitId',
|
'devilFruitId',
|
||||||
'hakiObservation',
|
'hakiObservation',
|
||||||
'hakiArmament',
|
'hakiArmament',
|
||||||
@@ -156,11 +176,16 @@ export async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const typeOrder: Record<'new' | 'modified' | 'deleted', number> = {
|
||||||
|
new: 0,
|
||||||
|
modified: 1,
|
||||||
|
deleted: 2
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
changes: changes.sort((a, b) => {
|
changes: changes.sort((a, b) => {
|
||||||
// Show 'new' first, then 'modified'
|
|
||||||
if (a.type !== b.type) {
|
if (a.type !== b.type) {
|
||||||
return a.type === 'new' ? -1 : 1;
|
return typeOrder[a.type] - typeOrder[b.type];
|
||||||
}
|
}
|
||||||
return a.id.localeCompare(b.id);
|
return a.id.localeCompare(b.id);
|
||||||
})
|
})
|
||||||
@@ -221,10 +246,10 @@ export const actions = {
|
|||||||
return { success: false, message: 'characterId is required' };
|
return { success: false, message: 'characterId is required' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const applied = await upsertCharacterFromScrapeValidation(characterId);
|
const applied = await applyCharacterChangeFromScrapeValidation(characterId);
|
||||||
return {
|
return {
|
||||||
success: applied,
|
success: applied,
|
||||||
message: applied ? 'Character applied successfully' : 'Character not found in scrape validation table'
|
message: applied ? 'Character change applied successfully' : 'Character not found in scrape validation table'
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -233,7 +258,7 @@ export const actions = {
|
|||||||
let appliedCount = 0;
|
let appliedCount = 0;
|
||||||
|
|
||||||
for (const scraped of scrapedCharacters) {
|
for (const scraped of scrapedCharacters) {
|
||||||
const applied = await upsertCharacterFromScrapeValidation(scraped.id);
|
const applied = await applyCharacterChangeFromScrapeValidation(scraped.id);
|
||||||
if (applied) {
|
if (applied) {
|
||||||
appliedCount++;
|
appliedCount++;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,30 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
type CharacterLike = {
|
||||||
|
name: string;
|
||||||
|
pictureUrl?: string | null;
|
||||||
|
url?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
gender?: string | null;
|
||||||
|
age?: number | null;
|
||||||
|
bounty?: number | null;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CharacterChange = {
|
||||||
|
type: 'new' | 'modified' | 'deleted';
|
||||||
|
id: string;
|
||||||
|
scraped: CharacterLike;
|
||||||
|
current?: CharacterLike;
|
||||||
|
differences?: Record<string, { current: unknown; scraped: unknown }>;
|
||||||
|
};
|
||||||
|
|
||||||
let { data, form } = $props();
|
let { data, form } = $props();
|
||||||
|
|
||||||
const newCharacters = $derived(data.changes.filter((c: any) => c.type === 'new'));
|
const newCharacters = $derived((data.changes as CharacterChange[]).filter((c) => c.type === 'new'));
|
||||||
const modifiedCharacters = $derived(data.changes.filter((c: any) => c.type === 'modified'));
|
const modifiedCharacters = $derived((data.changes as CharacterChange[]).filter((c) => c.type === 'modified'));
|
||||||
|
const deletedCharacters = $derived((data.changes as CharacterChange[]).filter((c) => c.type === 'deleted'));
|
||||||
|
|
||||||
function formatValue(value: any): string {
|
function formatValue(value: unknown): string {
|
||||||
if (value === null || value === undefined) {
|
if (value === null || value === undefined) {
|
||||||
return '—';
|
return '—';
|
||||||
}
|
}
|
||||||
@@ -25,7 +45,7 @@
|
|||||||
<div class="space-y-8">
|
<div class="space-y-8">
|
||||||
<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, {deletedCharacters.length} deleted</p>
|
||||||
<form method="POST" action="?/runScrapeImport" class="mt-4">
|
<form method="POST" action="?/runScrapeImport" class="mt-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -42,7 +62,7 @@
|
|||||||
{#if form?.logs}
|
{#if form?.logs}
|
||||||
<pre class="mt-3 max-h-72 overflow-auto rounded-lg border border-white/10 bg-slate-900/70 p-3 text-xs text-slate-200 whitespace-pre-wrap">{form.logs}</pre>
|
<pre class="mt-3 max-h-72 overflow-auto rounded-lg border border-white/10 bg-slate-900/70 p-3 text-xs text-slate-200 whitespace-pre-wrap">{form.logs}</pre>
|
||||||
{/if}
|
{/if}
|
||||||
{#if newCharacters.length + modifiedCharacters.length > 0}
|
{#if newCharacters.length + modifiedCharacters.length + deletedCharacters.length > 0}
|
||||||
<form method="POST" action="?/acceptAll" class="mt-4">
|
<form method="POST" action="?/acceptAll" class="mt-4">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -68,7 +88,7 @@
|
|||||||
{#if change.scraped.pictureUrl}
|
{#if change.scraped.pictureUrl}
|
||||||
<a href="https://onepiece.fandom.com/fr/wiki/{change.scraped.url}" target="_blank" rel="noopener noreferrer">
|
<a href="https://onepiece.fandom.com/fr/wiki/{change.scraped.url}" target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
src={change.scraped.pictureUrl}
|
src={change.scraped.pictureUrl ?? undefined}
|
||||||
alt={change.scraped.name}
|
alt={change.scraped.name}
|
||||||
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
|
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
|
||||||
/>
|
/>
|
||||||
@@ -127,8 +147,8 @@
|
|||||||
{#if change.current?.pictureUrl}
|
{#if change.current?.pictureUrl}
|
||||||
<a href="https://onepiece.fandom.com/fr/wiki/{change.current?.url ?? change.scraped.url}" target="_blank" rel="noopener noreferrer">
|
<a href="https://onepiece.fandom.com/fr/wiki/{change.current?.url ?? change.scraped.url}" target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
src={change.current.pictureUrl}
|
src={change.current?.pictureUrl ?? undefined}
|
||||||
alt={change.current.name}
|
alt={change.current?.name ?? change.scraped.name}
|
||||||
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
|
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
@@ -174,7 +194,49 @@
|
|||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if newCharacters.length === 0 && modifiedCharacters.length === 0}
|
<!-- Deleted Characters Section -->
|
||||||
|
{#if deletedCharacters.length > 0}
|
||||||
|
<section class="space-y-4">
|
||||||
|
<h2 class="text-xl font-bold text-rose-400 uppercase tracking-[0.15em]">
|
||||||
|
🗑️ Deleted Characters ({deletedCharacters.length})
|
||||||
|
</h2>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
{#each deletedCharacters as change (change.id)}
|
||||||
|
<div class="rounded-lg border border-rose-500/30 bg-rose-500/5 p-4 space-y-3">
|
||||||
|
<div class="flex items-center justify-between gap-3">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
{#if change.current?.pictureUrl}
|
||||||
|
<a href="https://onepiece.fandom.com/fr/wiki/{change.current?.url ?? change.scraped.url}" target="_blank" rel="noopener noreferrer">
|
||||||
|
<img
|
||||||
|
src={change.current?.pictureUrl ?? undefined}
|
||||||
|
alt={change.current?.name ?? change.scraped.name}
|
||||||
|
class="w-12 h-12 rounded object-cover hover:opacity-80 transition"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<div>
|
||||||
|
<h3 class="font-bold text-rose-300">{change.current?.name ?? change.scraped.name}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{change.id}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="POST" action="?/acceptOne">
|
||||||
|
<input type="hidden" name="characterId" value={change.id} />
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-full border border-rose-300/40 bg-rose-500/20 px-3 py-1 text-xs font-semibold text-rose-100 transition hover:bg-rose-500/30"
|
||||||
|
>
|
||||||
|
Supprimer
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-rose-200/80">This character is no longer present in the latest scrape and will be removed if accepted.</p>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if newCharacters.length === 0 && modifiedCharacters.length === 0 && deletedCharacters.length === 0}
|
||||||
<div class="rounded-lg border border-white/10 bg-white/5 p-8 text-center">
|
<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>
|
<p class="text-gray-400">Aucun changement détecté. Les tables character et characterScrapeValidation sont synchronisées.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
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';
|
// Helper function to normalize data (parse JSON arrays)
|
||||||
import { existsSync, mkdirSync } from 'fs';
|
const normalizeArray = (value: any): any => {
|
||||||
import { env } from '$env/dynamic/private';
|
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 () => {
|
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
|
db
|
||||||
.select({
|
.select({
|
||||||
id: character.id,
|
id: character.id,
|
||||||
name: character.name,
|
name: character.name,
|
||||||
gender: character.gender,
|
gender: character.gender,
|
||||||
age: character.age,
|
age: character.age,
|
||||||
affiliations: character.affiliations,
|
affiliation: character.affiliation,
|
||||||
devilFruitId: character.devilFruitId,
|
devilFruitId: character.devilFruitId,
|
||||||
hakiObservation: character.hakiObservation,
|
hakiObservation: character.hakiObservation,
|
||||||
hakiArmament: character.hakiArmament,
|
hakiArmament: character.hakiArmament,
|
||||||
@@ -26,7 +36,7 @@ export const load: PageServerLoad = async () => {
|
|||||||
origin: character.origin,
|
origin: character.origin,
|
||||||
firstAppearance: character.firstAppearance,
|
firstAppearance: character.firstAppearance,
|
||||||
pictureUrl: character.pictureUrl,
|
pictureUrl: character.pictureUrl,
|
||||||
epithets: character.epithets,
|
epithets: normalizeArray(character.epithets),
|
||||||
status: character.status,
|
status: character.status,
|
||||||
url: character.url,
|
url: character.url,
|
||||||
arcId: character.arcId,
|
arcId: character.arcId,
|
||||||
@@ -41,7 +51,6 @@ export const load: PageServerLoad = async () => {
|
|||||||
.orderBy(character.name),
|
.orderBy(character.name),
|
||||||
db.select().from(devilFruit).orderBy(devilFruit.name),
|
db.select().from(devilFruit).orderBy(devilFruit.name),
|
||||||
db.select().from(arc).orderBy(arc.name),
|
db.select().from(arc).orderBy(arc.name),
|
||||||
db.select().from(characterOverride),
|
|
||||||
db.selectDistinct({ status: character.status })
|
db.selectDistinct({ status: character.status })
|
||||||
.from(character)
|
.from(character)
|
||||||
.where(sql`${character.status} IS NOT NULL AND ${character.status} != ''`),
|
.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} != ''`)
|
.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 {
|
return {
|
||||||
characters: charactersWithOverrides,
|
characters,
|
||||||
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' });
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
bounty: 0,
|
bounty: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
origin: '',
|
origin: '',
|
||||||
affiliations: '',
|
affiliation: '',
|
||||||
epithets: '',
|
epithets: '',
|
||||||
pictureUrl: '',
|
pictureUrl: '',
|
||||||
url: '',
|
url: '',
|
||||||
@@ -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;
|
||||||
});
|
});
|
||||||
@@ -102,7 +101,7 @@
|
|||||||
bounty: override.bounty ?? null,
|
bounty: override.bounty ?? null,
|
||||||
height: override.height ?? null,
|
height: override.height ?? null,
|
||||||
origin: override.origin ?? '',
|
origin: override.origin ?? '',
|
||||||
affiliations: override.affiliations ?? '',
|
affiliation: override.affiliation ?? '',
|
||||||
epithets: override.epithets ?? '',
|
epithets: override.epithets ?? '',
|
||||||
pictureUrl: override.pictureUrl ?? '',
|
pictureUrl: override.pictureUrl ?? '',
|
||||||
url: override.url ?? '',
|
url: override.url ?? '',
|
||||||
@@ -128,7 +127,7 @@
|
|||||||
bounty: 0,
|
bounty: 0,
|
||||||
height: 0,
|
height: 0,
|
||||||
origin: '',
|
origin: '',
|
||||||
affiliations: '',
|
affiliation: '',
|
||||||
epithets: '',
|
epithets: '',
|
||||||
pictureUrl: '',
|
pictureUrl: '',
|
||||||
url: '',
|
url: '',
|
||||||
@@ -277,116 +276,112 @@
|
|||||||
{#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/fr/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/fr/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.affiliation}
|
||||||
{#if Array.isArray(char.displayValues.affiliations) && char.displayValues.affiliations.length > 0}
|
{char.affiliation}
|
||||||
<span class="inline-block" title={char.displayValues.affiliations.join(', ')}>{char.displayValues.affiliations[0]}</span>
|
|
||||||
{:else}
|
|
||||||
{char.displayValues.affiliations}
|
|
||||||
{/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 +399,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();
|
||||||
@@ -797,4 +792,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { characterHistory, config, friendship, user, userCharacterHistory } from '$lib/server/db/schema';
|
import { character, characterHistory, config, friendship, user, userCharacterHistory } from '$lib/server/db/schema';
|
||||||
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter, getTodayCharacterWinsCount, getDateKey } from '$lib/server/daily-character';
|
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter, getTodayCharacterWinsCount, getDateKey } from '$lib/server/daily-character';
|
||||||
import { and, eq, inArray, like, or } from 'drizzle-orm';
|
import { and, eq, inArray, like, or } from 'drizzle-orm';
|
||||||
|
|
||||||
@@ -17,7 +17,13 @@ export async function load(event) {
|
|||||||
// Load the win count for today
|
// Load the win count for today
|
||||||
const winCount = await getTodayCharacterWinsCount(dailyCharacter.id);
|
const winCount = await getTodayCharacterWinsCount(dailyCharacter.id);
|
||||||
|
|
||||||
let friendsTodayResults: Array<{ userId: string; name: string; image: string | null; tryCount: number }> = [];
|
let friendsTodayResults: Array<{
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
image: string | null;
|
||||||
|
tryCount: number;
|
||||||
|
triedCharacters: Array<{ id: string; name: string; pictureUrl: string | null }>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
const currentUserId = event.locals.user.id;
|
const currentUserId = event.locals.user.id;
|
||||||
@@ -51,12 +57,13 @@ export async function load(event) {
|
|||||||
const todayCharacterHistoryId = todayHistoryEntry?.id;
|
const todayCharacterHistoryId = todayHistoryEntry?.id;
|
||||||
|
|
||||||
if (todayCharacterHistoryId) {
|
if (todayCharacterHistoryId) {
|
||||||
friendsTodayResults = await db
|
const friendResultsRaw = await db
|
||||||
.select({
|
.select({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
image: user.image,
|
image: user.image,
|
||||||
tryCount: userCharacterHistory.tryCount
|
tryCount: userCharacterHistory.tryCount,
|
||||||
|
triedCharacterIds: userCharacterHistory.triedCharacterIds
|
||||||
})
|
})
|
||||||
.from(userCharacterHistory)
|
.from(userCharacterHistory)
|
||||||
.innerJoin(user, eq(userCharacterHistory.userId, user.id))
|
.innerJoin(user, eq(userCharacterHistory.userId, user.id))
|
||||||
@@ -67,6 +74,33 @@ export async function load(event) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(userCharacterHistory.tryCount);
|
.orderBy(userCharacterHistory.tryCount);
|
||||||
|
|
||||||
|
const uniqueTriedCharacterIds = Array.from(new Set(
|
||||||
|
friendResultsRaw.flatMap((entry) => entry.triedCharacterIds ?? [])
|
||||||
|
));
|
||||||
|
|
||||||
|
const triedCharacters = uniqueTriedCharacterIds.length > 0
|
||||||
|
? await db
|
||||||
|
.select({
|
||||||
|
id: character.id,
|
||||||
|
name: character.name,
|
||||||
|
pictureUrl: character.pictureUrl
|
||||||
|
})
|
||||||
|
.from(character)
|
||||||
|
.where(inArray(character.id, uniqueTriedCharacterIds))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const triedCharactersById = new Map(triedCharacters.map((entry) => [entry.id, entry]));
|
||||||
|
|
||||||
|
friendsTodayResults = friendResultsRaw.map((entry) => ({
|
||||||
|
userId: entry.userId,
|
||||||
|
name: entry.name,
|
||||||
|
image: entry.image,
|
||||||
|
tryCount: entry.tryCount,
|
||||||
|
triedCharacters: (entry.triedCharacterIds ?? [])
|
||||||
|
.map((characterId) => triedCharactersById.get(characterId))
|
||||||
|
.filter((triedEntry): triedEntry is (typeof triedCharacters)[number] => !!triedEntry)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
||||||
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
|
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
|
||||||
import WinPanel from '$lib/components/WinPanel.svelte';
|
import WinPanel from '$lib/components/WinPanel.svelte';
|
||||||
|
import FriendsTodaySection from '$lib/components/FriendsTodaySection.svelte';
|
||||||
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
|
|
||||||
@@ -159,6 +160,8 @@
|
|||||||
|
|
||||||
// Check if player won
|
// Check if player won
|
||||||
if (character.id === dailyCharacter.id) {
|
if (character.id === dailyCharacter.id) {
|
||||||
|
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
|
||||||
|
|
||||||
// Send request to record win in database
|
// Send request to record win in database
|
||||||
fetch('/daily', {
|
fetch('/daily', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -167,7 +170,8 @@
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
characterId: dailyCharacter.id,
|
characterId: dailyCharacter.id,
|
||||||
tryCount: selectedCharacters.length
|
tryCount: selectedCharacters.length,
|
||||||
|
triedCharacterIds
|
||||||
})
|
})
|
||||||
}).catch(err => console.error('Failed to record win:', err));
|
}).catch(err => console.error('Failed to record win:', err));
|
||||||
|
|
||||||
@@ -313,32 +317,7 @@
|
|||||||
|
|
||||||
|
|
||||||
{#if hasWon && data.friendsTodayResults && data.friendsTodayResults.length > 0}
|
{#if hasWon && data.friendsTodayResults && data.friendsTodayResults.length > 0}
|
||||||
<section class="mt-6 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
<FriendsTodaySection friendsTodayResults={data.friendsTodayResults} />
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">{$t.game.daily.friendsToday}</p>
|
|
||||||
<div class="mt-4 space-y-2">
|
|
||||||
{#each data.friendsTodayResults as friendResult (friendResult.userId)}
|
|
||||||
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-slate-950/50 px-4 py-2">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
{#if friendResult.image}
|
|
||||||
<img
|
|
||||||
src={friendResult.image}
|
|
||||||
alt={friendResult.name}
|
|
||||||
class="h-8 w-8 rounded-full border border-white/20 object-cover"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-amber-300/20 text-xs font-semibold text-amber-100">
|
|
||||||
{friendResult.name?.charAt(0).toUpperCase() || 'U'}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<p class="text-sm font-semibold text-slate-100">{friendResult.name}</p>
|
|
||||||
</div>
|
|
||||||
<p class="text-sm text-amber-300">
|
|
||||||
{friendResult.tryCount} {friendResult.tryCount > 1 ? $t.game.daily.friendTryPlural : $t.game.daily.friendTrySingular}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<GuessHistoryTable
|
<GuessHistoryTable
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { getDateKey } from '$lib/server/daily-character';
|
|||||||
|
|
||||||
export async function POST({ request, locals }) {
|
export async function POST({ request, locals }) {
|
||||||
try {
|
try {
|
||||||
const { characterId, tryCount } = await request.json();
|
const { characterId, tryCount, triedCharacterIds } = await request.json();
|
||||||
|
const normalizedTriedCharacterIds = Array.isArray(triedCharacterIds)
|
||||||
|
? triedCharacterIds.filter((id): id is string => typeof id === 'string')
|
||||||
|
: [];
|
||||||
|
|
||||||
if (!characterId) {
|
if (!characterId) {
|
||||||
return json({ error: 'Missing characterId' }, { status: 400 });
|
return json({ error: 'Missing characterId' }, { status: 400 });
|
||||||
@@ -51,7 +54,8 @@ export async function POST({ request, locals }) {
|
|||||||
await db.insert(userCharacterHistory).values({
|
await db.insert(userCharacterHistory).values({
|
||||||
userId: locals.user.id,
|
userId: locals.user.id,
|
||||||
characterHistoryId: todayHistoryEntry.id,
|
characterHistoryId: todayHistoryEntry.id,
|
||||||
tryCount: tryCount
|
tryCount: tryCount,
|
||||||
|
triedCharacterIds: normalizedTriedCharacterIds
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
hasDevilFruit: null as boolean | null, // null = all, true = with fruit, false = without fruit
|
hasDevilFruit: null as boolean | null, // null = all, true = with fruit, false = without fruit
|
||||||
status: [] as string[],
|
status: [] as string[],
|
||||||
hasHeight: false,
|
hasHeight: false,
|
||||||
|
hasAge: false,
|
||||||
hasOrigin: false,
|
hasOrigin: false,
|
||||||
arcs: [] as string[]
|
arcs: [] as string[]
|
||||||
};
|
};
|
||||||
@@ -141,6 +142,9 @@
|
|||||||
if (!characterFilters.arcs) {
|
if (!characterFilters.arcs) {
|
||||||
characterFilters.arcs = [];
|
characterFilters.arcs = [];
|
||||||
}
|
}
|
||||||
|
if (typeof characterFilters.hasAge !== 'boolean') {
|
||||||
|
characterFilters.hasAge = false;
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to parse filters', e);
|
console.error('Failed to parse filters', e);
|
||||||
}
|
}
|
||||||
@@ -276,6 +280,11 @@
|
|||||||
if (characterFilters.hasHeight && (char.height === null || char.height === undefined)) {
|
if (characterFilters.hasHeight && (char.height === null || char.height === undefined)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Age filter
|
||||||
|
if (characterFilters.hasAge && (char.age === null || char.age === undefined)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Origin filter
|
// Origin filter
|
||||||
if (characterFilters.hasOrigin && (char.origin === null || char.origin === undefined || char.origin === '')) {
|
if (characterFilters.hasOrigin && (char.origin === null || char.origin === undefined || char.origin === '')) {
|
||||||
@@ -306,6 +315,7 @@
|
|||||||
haki: $t.game.components.guessHistory.haki,
|
haki: $t.game.components.guessHistory.haki,
|
||||||
bounty: $t.game.components.guessHistory.bounty,
|
bounty: $t.game.components.guessHistory.bounty,
|
||||||
height: $t.game.components.guessHistory.height,
|
height: $t.game.components.guessHistory.height,
|
||||||
|
age: $t.game.components.guessHistory.age,
|
||||||
origin: $t.game.components.guessHistory.origin,
|
origin: $t.game.components.guessHistory.origin,
|
||||||
arc: $t.game.components.guessHistory.arc
|
arc: $t.game.components.guessHistory.arc
|
||||||
};
|
};
|
||||||
@@ -424,6 +434,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAgeFilter() {
|
||||||
|
characterFilters.hasAge = !characterFilters.hasAge;
|
||||||
|
if (!hasWon) {
|
||||||
|
generateNewCharacter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleOriginFilter() {
|
function toggleOriginFilter() {
|
||||||
characterFilters.hasOrigin = !characterFilters.hasOrigin;
|
characterFilters.hasOrigin = !characterFilters.hasOrigin;
|
||||||
// Regenerate character with new filters
|
// Regenerate character with new filters
|
||||||
@@ -451,6 +468,7 @@
|
|||||||
hasDevilFruit: null,
|
hasDevilFruit: null,
|
||||||
status: [],
|
status: [],
|
||||||
hasHeight: false,
|
hasHeight: false,
|
||||||
|
hasAge: false,
|
||||||
hasOrigin: false,
|
hasOrigin: false,
|
||||||
arcs: []
|
arcs: []
|
||||||
};
|
};
|
||||||
@@ -661,7 +679,7 @@
|
|||||||
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
|
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
|
||||||
{$t.game.infinite.filtersTitle}
|
{$t.game.infinite.filtersTitle}
|
||||||
</h3>
|
</h3>
|
||||||
{#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasOrigin || characterFilters.arcs.length > 0}
|
{#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasAge || characterFilters.hasOrigin || characterFilters.arcs.length > 0}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={clearAllFilters}
|
onclick={clearAllFilters}
|
||||||
@@ -758,6 +776,15 @@
|
|||||||
>
|
>
|
||||||
{$t.game.infinite.heightDefined}
|
{$t.game.infinite.heightDefined}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={toggleAgeFilter}
|
||||||
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasAge
|
||||||
|
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
|
||||||
|
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
|
||||||
|
>
|
||||||
|
{$t.game.infinite.ageDefined}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onclick={toggleOriginFilter}
|
onclick={toggleOriginFilter}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Actions, PageServerLoad } from './$types';
|
|||||||
import { auth } from '$lib/server/auth';
|
import { auth } from '$lib/server/auth';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema';
|
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema';
|
||||||
import { and, desc, eq, or, sql } from 'drizzle-orm';
|
import { and, desc, eq, inArray, or, sql } from 'drizzle-orm';
|
||||||
import { APIError } from 'better-auth/api';
|
import { APIError } from 'better-auth/api';
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
@@ -20,12 +20,13 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
.where(eq(session.userId, event.locals.user.id));
|
.where(eq(session.userId, event.locals.user.id));
|
||||||
|
|
||||||
// Fetch daily history for this user
|
// Fetch daily history for this user
|
||||||
const dailyHistory = await db
|
const dailyHistoryRaw = await db
|
||||||
.select({
|
.select({
|
||||||
id: userCharacterHistory.id,
|
id: userCharacterHistory.id,
|
||||||
characterId: characterHistory.characterId,
|
characterId: characterHistory.characterId,
|
||||||
date: characterHistory.date,
|
date: characterHistory.date,
|
||||||
tryCount: userCharacterHistory.tryCount,
|
tryCount: userCharacterHistory.tryCount,
|
||||||
|
triedCharacterIds: userCharacterHistory.triedCharacterIds,
|
||||||
won: characterHistory.won,
|
won: characterHistory.won,
|
||||||
characterName: character.name,
|
characterName: character.name,
|
||||||
characterImage: character.pictureUrl
|
characterImage: character.pictureUrl
|
||||||
@@ -36,6 +37,30 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
.where(eq(userCharacterHistory.userId, event.locals.user.id))
|
.where(eq(userCharacterHistory.userId, event.locals.user.id))
|
||||||
.orderBy(desc(characterHistory.date));
|
.orderBy(desc(characterHistory.date));
|
||||||
|
|
||||||
|
const uniqueTriedCharacterIds = Array.from(new Set(
|
||||||
|
dailyHistoryRaw.flatMap((entry) => entry.triedCharacterIds ?? [])
|
||||||
|
));
|
||||||
|
|
||||||
|
const triedCharacters = uniqueTriedCharacterIds.length > 0
|
||||||
|
? await db
|
||||||
|
.select({
|
||||||
|
id: character.id,
|
||||||
|
name: character.name,
|
||||||
|
pictureUrl: character.pictureUrl
|
||||||
|
})
|
||||||
|
.from(character)
|
||||||
|
.where(inArray(character.id, uniqueTriedCharacterIds))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const triedCharactersById = new Map(triedCharacters.map((entry) => [entry.id, entry]));
|
||||||
|
|
||||||
|
const dailyHistory = dailyHistoryRaw.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
triedCharacters: (entry.triedCharacterIds ?? [])
|
||||||
|
.map((characterId) => triedCharactersById.get(characterId))
|
||||||
|
.filter((triedEntry): triedEntry is (typeof triedCharacters)[number] => !!triedEntry)
|
||||||
|
}));
|
||||||
|
|
||||||
const incomingRequests = await db
|
const incomingRequests = await db
|
||||||
.select({
|
.select({
|
||||||
id: friendship.id,
|
id: friendship.id,
|
||||||
|
|||||||
@@ -9,6 +9,21 @@
|
|||||||
form?: { success?: boolean; message?: string } | null;
|
form?: { success?: boolean; message?: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DailyHistoryEntry {
|
||||||
|
id: string;
|
||||||
|
characterId: string | null;
|
||||||
|
date: number;
|
||||||
|
tryCount: number;
|
||||||
|
won: number;
|
||||||
|
characterName: string;
|
||||||
|
characterImage: string | null;
|
||||||
|
triedCharacters?: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
pictureUrl: string | null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
@@ -20,7 +35,7 @@
|
|||||||
let newPassword = $state('');
|
let newPassword = $state('');
|
||||||
let confirmPassword = $state('');
|
let confirmPassword = $state('');
|
||||||
let sessions = $derived(data.sessions || []);
|
let sessions = $derived(data.sessions || []);
|
||||||
let dailyHistory = $derived(data.dailyHistory || []);
|
let dailyHistory = $derived((data.dailyHistory || []) as DailyHistoryEntry[]);
|
||||||
let friends = $derived(data.friends || []);
|
let friends = $derived(data.friends || []);
|
||||||
let incomingRequests = $derived(data.incomingRequests || []);
|
let incomingRequests = $derived(data.incomingRequests || []);
|
||||||
let outgoingRequests = $derived(data.outgoingRequests || []);
|
let outgoingRequests = $derived(data.outgoingRequests || []);
|
||||||
@@ -455,6 +470,29 @@
|
|||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||||
|
{$t.game.profile.triedCharactersTitle}
|
||||||
|
</p>
|
||||||
|
{#if day.triedCharacters && day.triedCharacters.length > 0}
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
{#each day.triedCharacters as triedCharacter (triedCharacter.id)}
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-2.5 py-1 text-xs text-slate-200">
|
||||||
|
{#if triedCharacter.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={triedCharacter.pictureUrl}
|
||||||
|
alt={triedCharacter.name}
|
||||||
|
class="h-4 w-4 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{triedCharacter.name}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{$t.game.profile.noTriedCharacters}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tries -->
|
<!-- Tries -->
|
||||||
|
|||||||
Reference in New Issue
Block a user