feat: add WinPanel and YesterdayCharacter components; refactor daily game page

- Introduced WinPanel component to display win/loss messages based on game outcome.
- Added YesterdayCharacter component to show the character from the previous day.
- Refactored daily game page to utilize new components for better code organization and readability.
- Updated server-side logic to fetch daily character ID and count wins more efficiently.
- Cleaned up character selection logic and hint availability tracking.
This commit is contained in:
2026-03-02 11:05:02 +01:00
parent 4d059d45ef
commit cf25a96719
7 changed files with 649 additions and 555 deletions

View File

@@ -0,0 +1,167 @@
<script lang="ts">
import { onMount } from 'svelte';
import { createEventDispatcher } from 'svelte';
export let characters: any[];
export let selectedCharacters: any[];
const dispatch = createEventDispatcher();
let searchInput = '';
let highlightedIndex = 0;
let dropdownContainer: HTMLDivElement;
let searchContainer: HTMLDivElement;
onMount(() => {
// Add click outside listener
document.addEventListener('click', handleClickOutside);
return () => {
document.removeEventListener('click', handleClickOutside);
};
});
$: filteredCharacters = characters.filter(char => {
const searchTerm = searchInput.toLowerCase();
const nameMatches = char.name.toLowerCase().includes(searchTerm);
let epithetsMatches = false;
if (char.epithets) {
try {
const parsedEpithets = typeof char.epithets === 'string'
? JSON.parse(char.epithets)
: char.epithets;
if (Array.isArray(parsedEpithets)) {
epithetsMatches = parsedEpithets.some((epithet: string) =>
epithet.toLowerCase().includes(searchTerm)
);
} else if (typeof parsedEpithets === 'string') {
epithetsMatches = parsedEpithets.toLowerCase().includes(searchTerm);
}
} catch {
epithetsMatches = String(char.epithets).toLowerCase().includes(searchTerm);
}
}
return (nameMatches || epithetsMatches) &&
!selectedCharacters.some(selected => selected.id === char.id);
});
// Reset highlighted index when filtered list changes
$: if (filteredCharacters) {
highlightedIndex = 0;
}
// Scroll highlighted item into view
$: if (dropdownContainer && highlightedIndex >= 0) {
const highlightedButton = dropdownContainer.querySelector(
`button:nth-child(${highlightedIndex + 1})`
) as HTMLElement;
if (highlightedButton) {
highlightedButton.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}
function selectCharacter(character: any) {
dispatch('select', character);
searchInput = '';
highlightedIndex = 0;
}
function handleKeydown(event: KeyboardEvent) {
if (filteredCharacters.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
highlightedIndex = Math.min(highlightedIndex + 1, filteredCharacters.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
highlightedIndex = Math.max(highlightedIndex - 1, 0);
break;
case 'Enter':
event.preventDefault();
if (filteredCharacters[highlightedIndex]) {
selectCharacter(filteredCharacters[highlightedIndex]);
}
break;
}
}
function submitGuess() {
if (filteredCharacters.length === 0) return;
const characterToSelect =
filteredCharacters[highlightedIndex] ?? filteredCharacters[0];
if (characterToSelect) {
selectCharacter(characterToSelect);
}
}
function handleClickOutside(event: MouseEvent) {
if (searchContainer && !searchContainer.contains(event.target as Node)) {
searchInput = '';
}
}
</script>
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur z-10">
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Entrer une supposition</h2>
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
<div bind:this={searchContainer} class="relative w-full">
<input
bind:value={searchInput}
class="w-full rounded-full border border-amber-200/30 bg-slate-900/60 px-5 py-3 text-sm text-slate-100 placeholder:text-slate-400 focus:border-amber-200/70 focus:outline-none"
placeholder="Nom du personnage"
type="text"
onkeydown={handleKeydown}
/>
{#if searchInput.length > 0 && filteredCharacters.length > 0}
<div bind:this={dropdownContainer} class="absolute top-full left-0 right-0 mt-2 max-h-96 overflow-y-auto rounded-2xl border border-amber-200/30 bg-slate-900/90 backdrop-blur z-10">
{#each filteredCharacters as character, index (character.id)}
<button
class="w-full px-5 py-4 text-left text-base text-slate-100 transition border-b border-slate-800/50 last:border-b-0 flex items-center gap-4 {index === highlightedIndex ? 'bg-slate-700' : 'hover:bg-slate-800/70'}"
type="button"
onmouseenter={() => highlightedIndex = index}
onclick={() => selectCharacter(character)}
>
{#if character.pictureUrl}
<img
src={character.pictureUrl}
alt={character.name}
class="w-12 h-12 rounded-full object-cover border border-amber-200/30"
/>
{:else}
<div class="w-12 h-12 rounded-full bg-slate-800 border border-amber-200/30 flex items-center justify-center">
<span class="text-xs text-slate-400">?</span>
</div>
{/if}
<div class="flex-1">
<span class="font-semibold text-amber-100">{character.name}</span>
{#if character.epithets}
{@const parsedEpithets = typeof character.epithets === 'string'
? JSON.parse(character.epithets)
: character.epithets}
{#if Array.isArray(parsedEpithets) && parsedEpithets.length > 0}
<span class="ml-2 text-xs text-slate-400">
{parsedEpithets.join(', ')}
</span>
{/if}
{/if}
</div>
</button>
{/each}
</div>
{/if}
</div>
<button
type="button"
onclick={submitGuess}
disabled={searchInput.length === 0 || filteredCharacters.length === 0}
class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200 disabled:cursor-not-allowed disabled:opacity-50"
>
Valider
</button>
</div>
</div>

View File

@@ -0,0 +1,246 @@
<script lang="ts">
import { formatBounty } from '$lib';
export let selectedCharacters: any[];
export let dailyCharacter: any;
export let columnVisibility: any;
</script>
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<div class="flex flex-col gap-4">
<div class="flex flex-col items-center gap-4 text-center">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p>
</div>
{#if selectedCharacters.length === 0}
<p class="text-sm text-slate-200 text-center">Aucune tentative pour le moment.</p>
{:else}
<div class="overflow-x-auto pb-2 -mx-6 px-6 sm:mx-0 sm:px-0">
<div class="w-max min-w-max mx-auto">
<!-- Header -->
<div class="flex gap-2 mb-2">
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Personnage</p>
</div>
{#if columnVisibility.status !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Statut</p>
</div>
{/if}
{#if columnVisibility.gender !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Genre</p>
</div>
{/if}
{#if columnVisibility.affiliations !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Affiliations</p>
</div>
{/if}
{#if columnVisibility.devilFruitType !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Fruit</p>
</div>
{/if}
{#if columnVisibility.haki !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Haki</p>
</div>
{/if}
{#if columnVisibility.bounty !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Prime</p>
</div>
{/if}
{#if columnVisibility.height !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Taille</p>
</div>
{/if}
{#if columnVisibility.origin !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Origine</p>
</div>
{/if}
{#if columnVisibility.arc !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Arc</p>
</div>
{/if}
</div>
<!-- Rows -->
{#each selectedCharacters as character (character.id)}
<div class="flex gap-2 mb-2">
<!-- Personnage -->
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 bg-slate-950/60 overflow-hidden">
{#if character.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + character.url}
target="_blank"
rel="noopener noreferrer"
class="block w-full h-full"
>
<img
src={character.pictureUrl}
alt={character.name}
class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
/>
</a>
{:else}
<div class="w-full h-full bg-slate-800 flex items-center justify-center p-2">
<span class="text-xl text-center font-semibold line-clamp-3">{character.name}</span>
</div>
{/if}
</div>
<!-- Vivant / Mort -->
{#if columnVisibility.status !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.status === dailyCharacter.status ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
<p class="text-sm font-bold text-white text-center">
{character.status === 'Alive' ? 'Vivant' : character.status === 'Deceased' || character.status === 'Dead' ? 'Mort' : character.status || 'Inconnu'}
</p>
</div>
{/if}
<!-- Genre -->
{#if columnVisibility.gender !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.gender === dailyCharacter.gender ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
<p class="text-base font-bold text-white text-center">
{character.gender === 'Male' ? 'Homme' : character.gender === 'Female' ? 'Femme' : character.gender || 'Inconnu'}
</p>
</div>
{/if}
<!-- Affiliations -->
{#if columnVisibility.affiliations !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
try {
const charAff = typeof character.affiliations === 'string'
? ((character.affiliations as string).includes('[') ? JSON.parse(character.affiliations) : (character.affiliations as string).split(',').map((a: string) => a.trim()))
: character.affiliations;
const dailyAff = typeof dailyCharacter.affiliations === 'string'
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
: dailyCharacter.affiliations;
const charFirstAff = Array.isArray(charAff) ? charAff[0] : charAff;
const dailyFirstAff = Array.isArray(dailyAff) ? dailyAff[0] : dailyAff;
return charFirstAff && dailyFirstAff && charFirstAff === dailyFirstAff ? 'bg-emerald-600/90' : 'bg-red-900/60';
} catch (e) {
return 'bg-slate-950/60';
}
})()} p-2 flex items-center justify-center overflow-hidden">
{#if character.affiliations}
{@const parsedAffiliations = typeof character.affiliations === 'string'
? (character.affiliations.includes('[') ? JSON.parse(character.affiliations) : character.affiliations.split(',').map((a: string) => a.trim()))
: character.affiliations}
{#if Array.isArray(parsedAffiliations) && parsedAffiliations.length > 0}
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations[0]}</p>
{:else}
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations}</p>
{/if}
{:else}
<p class="text-base font-bold text-slate-400 text-center">-</p>
{/if}
</div>
{/if}
<!-- Fruit -->
{#if columnVisibility.devilFruitType !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.devilFruitType === dailyCharacter.devilFruitType ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
{#if character.devilFruitType}
<p class="text-sm font-bold text-white text-center">{character.devilFruitType}</p>
{:else}
<p class="text-5xl font-bold text-white text-center"></p>
{/if}
</div>
{/if}
<!-- Haki -->
{#if columnVisibility.haki !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
if (character.hakiObservation === dailyCharacter.hakiObservation && character.hakiArmament === dailyCharacter.hakiArmament && character.hakiConqueror === dailyCharacter.hakiConqueror) {
return 'bg-emerald-600/90';
} else if ((character.hakiObservation && dailyCharacter.hakiObservation) ||
(character.hakiArmament && dailyCharacter.hakiArmament) ||
(character.hakiConqueror && dailyCharacter.hakiConqueror)) {
return 'bg-yellow-600/80';
} else {
return 'bg-red-900/60';
}
})()} p-2 flex items-center justify-center">
<p class="text-2xl font-bold text-white text-center">
{#if character.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
{#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
<span class="text-5xl"></span>
{/if}
</p>
</div>
{/if}
<!-- Prime -->
{#if columnVisibility.bounty !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.bounty === dailyCharacter.bounty ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
{#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
background-color: rgb(203, 213, 225);
clip-path: {character.bounty > dailyCharacter.bounty
? '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.bounty != null}
<p class="text-sm font-bold text-white text-center relative z-10">{formatBounty(character.bounty)} ฿</p>
{:else}
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
{/if}
</div>
{/if}
<!-- Taille -->
{#if columnVisibility.height !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.height === dailyCharacter.height ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
{#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
background-color: rgb(203, 213, 225);
clip-path: {character.height > dailyCharacter.height
? '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.height}
<p class="text-sm font-bold text-white text-center relative z-10">{character.height} m</p>
{:else}
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
{/if}
</div>
{/if}
<!-- Origine -->
{#if columnVisibility.origin !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.origin === dailyCharacter.origin ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
<p class="text-sm font-bold text-white text-center">{character.origin || 'Inconnue'}</p>
</div>
{/if}
<!-- Arc -->
{#if columnVisibility.arc !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.arcName === dailyCharacter.arcName ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
{#if character.arcName !== dailyCharacter.arcName && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
background-color: rgb(203, 213, 225);
clip-path: {character.firstAppearance > dailyCharacter.firstAppearance
? '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}
<p class="text-sm font-bold text-white text-center relative z-10">{character.arcName || 'Inconnu'}</p>
</div>
{/if}
</div>
{/each}
</div>
</div>
{/if}
</div>
</section>

View File

@@ -0,0 +1,88 @@
<script lang="ts">
export let dailyCharacter: any;
export let selectedCharacters: any[];
export let showOriginUnlock: boolean = false;
export let showFruitUnlock: boolean = false;
export let showAffiliationUnlock: boolean = false;
let showHintOrigin = false;
let showHintFruit = false;
let showHintAffiliation = false;
// Hint availability - indices are available after a certain number of guesses
$: isOriginAvailable = selectedCharacters.length >= 5;
$: isFruitAvailable = selectedCharacters.length >= 10;
$: isAffiliationAvailable = selectedCharacters.length >= 15;
</script>
<svelte:head>
<style>
@keyframes hint-unlock {
0% {
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
}
50% {
box-shadow: 0 0 20px rgba(251, 146, 60, 0.8);
}
100% {
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
}
}
.hint-unlocking {
animation: hint-unlock 0.6s ease-out;
}
</style>
</svelte:head>
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
<div class="grid gap-3 sm:grid-cols-3">
<button
type="button"
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isOriginAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showOriginUnlock ? 'hint-unlocking' : ''}"
disabled={!isOriginAvailable}
onclick={() => showHintOrigin = !showHintOrigin}
>
<p class="text-sm font-medium text-amber-100">Origine</p>
{#if showHintOrigin}
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.origin || 'Inconnue'}</p>
{:else if Math.max(0, 5 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} essais avant déblocage</p>
{:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if}
</button>
<button
type="button"
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isFruitAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showFruitUnlock ? 'hint-unlocking' : ''}"
disabled={!isFruitAvailable}
onclick={() => showHintFruit = !showHintFruit}
>
<p class="text-sm font-medium text-amber-100">Fruit du démon</p>
{#if showHintFruit}
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || 'Aucun'}</p>
{:else if Math.max(0, 10 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} essais avant déblocage</p>
{:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if}
</button>
<button
type="button"
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isAffiliationAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showAffiliationUnlock ? 'hint-unlocking' : ''}"
disabled={!isAffiliationAvailable}
onclick={() => showHintAffiliation = !showHintAffiliation}
>
<p class="text-sm font-medium text-amber-100">Affiliation</p>
{#if showHintAffiliation}
{@const affiliations = typeof dailyCharacter.affiliations === 'string'
? ((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 || 'Inconnue'}</p>
{:else if Math.max(0, 15 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} essais avant déblocage</p>
{:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if}
</button>
</div>
</div>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
export let dailyCharacter: any;
export let selectedCharacters: any[];
export let isGeckoMoriaWin: boolean = false;
</script>
{#if isGeckoMoriaWin}
<div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect">
<div class="text-center">
<div class="text-3xl mb-2">🌑</div>
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<div class="mt-3">
{#if dailyCharacter.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="inline-block"
>
<img
src={dailyCharacter.pictureUrl}
alt={dailyCharacter.name}
class="w-20 h-20 mx-auto rounded-full border-2 border-slate-600 shadow-lg object-cover hover:border-slate-500 transition-colors cursor-pointer opacity-80"
/>
</a>
{/if}
<p class="mt-2 text-lg font-bold text-slate-200">{dailyCharacter.name}</p>
</div>
</div>
</div>
{:else}
<div class="rounded-3xl border border-emerald-500/50 bg-emerald-500/10 p-4 shadow-[0_24px_60px_rgba(16,185,129,0.3)] backdrop-blur">
<div class="text-center">
<div class="text-3xl mb-2">🎉</div>
<h2 class="text-xl font-bold text-emerald-400 mb-1">Félicitations !</h2>
<p class="text-sm text-emerald-300">Vous avez trouvé le personnage en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<div class="mt-3">
{#if dailyCharacter.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="inline-block"
>
<img
src={dailyCharacter.pictureUrl}
alt={dailyCharacter.name}
class="w-20 h-20 mx-auto rounded-full border-2 border-emerald-400 shadow-lg object-cover hover:border-emerald-300 transition-colors cursor-pointer"
/>
</a>
{/if}
<p class="mt-2 text-lg font-bold text-white">{dailyCharacter.name}</p>
</div>
</div>
</div>
{/if}

View File

@@ -0,0 +1,51 @@
<script lang="ts">
export let yesterdayCharacter: any;
</script>
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
{#if yesterdayCharacter}
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
{#if yesterdayCharacter.pictureUrl}
<img
src={yesterdayCharacter.pictureUrl}
alt={yesterdayCharacter.name}
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
/>
{:else}
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo
</div>
{/if}
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if yesterdayCharacter.epithets}
<p class="mt-1 text-sm text-slate-400">
{typeof yesterdayCharacter.epithets === 'string'
? JSON.parse(yesterdayCharacter.epithets).join(', ')
: (yesterdayCharacter.epithets as string[]).join(', ')}
</p>
{/if}
</div>
<a
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
>
Voir la page
</a>
</div>
{:else}
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo
</div>
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
</div>
</div>
{/if}
</section>

View File

@@ -1,7 +1,7 @@
import { db } from '$lib/server/db'; import { db } from '$lib/server/db';
import { character, devilFruit, arc, user, config, characterHistory } from '$lib/server/db/schema'; import { character, devilFruit, arc, user, config, characterHistory } from '$lib/server/db/schema';
import { eq } from 'drizzle-orm';
import type { PageServerLoad } from './$types'; import type { PageServerLoad } from './$types';
import { getDailyCharacterId } from '$lib/server/dailyCharacter';
export const load: PageServerLoad = async () => { export const load: PageServerLoad = async () => {
const [characters, devilFruits, arcs, users, configEntries, history] = await Promise.all([ const [characters, devilFruits, arcs, users, configEntries, history] = await Promise.all([
@@ -13,15 +13,9 @@ export const load: PageServerLoad = async () => {
db.select().from(characterHistory) db.select().from(characterHistory)
]); ]);
// Get daily character ID from config const dailyCharacterId = await getDailyCharacterId();
const dailyCharIdEntry = configEntries.find((c) => c.key === 'dailyCharacterId'); // Return count of wins for the daily character , count is stored in won field of characterHistory
const dailyCharId = dailyCharIdEntry?.value; const dailyCharacterWins = history.filter((h) => h.characterId === dailyCharacterId && h.won).length;
// Count how many times today's daily character was won/found
const today = new Date().toISOString().split('T')[0];
const dailyCharacterWins = dailyCharId
? history.filter((h) => h.characterId === dailyCharId && h.date === today && h.won === 1).length
: 0;
return { return {
stats: { stats: {

View File

@@ -1,18 +1,16 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { formatBounty } from '$lib'; import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
import HintsPanel from '$lib/components/HintsPanel.svelte';
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
import WinPanel from '$lib/components/WinPanel.svelte';
export let data; export let data;
let searchInput = '';
let selectedCharacters: any[] = []; let selectedCharacters: any[] = [];
let highlightedIndex = 0;
let isLoaded = false; let isLoaded = false;
let isGeckoMoriaWin = false; let isGeckoMoriaWin = false;
let dropdownContainer: HTMLDivElement;
let showHintOrigin = false;
let showHintFruit = false;
let showHintAffiliation = false;
let wasOriginAvailable = false; let wasOriginAvailable = false;
let wasFruitAvailable = false; let wasFruitAvailable = false;
@@ -68,10 +66,10 @@
$: columnVisibility = data.columnVisibility || {}; $: columnVisibility = data.columnVisibility || {};
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id); $: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id);
// Hint availability - indices are available after a certain number of guesses // Hint availability tracking for unlock animations
$: isOriginAvailable = selectedCharacters.length >= 5; // Always available $: isOriginAvailable = selectedCharacters.length >= 5;
$: isFruitAvailable = selectedCharacters.length >= 10; // Available after 5 guesses $: isFruitAvailable = selectedCharacters.length >= 10;
$: isAffiliationAvailable = selectedCharacters.length >= 15; // Available after 10 guesses $: isAffiliationAvailable = selectedCharacters.length >= 15;
// Track hint unlocks // Track hint unlocks
$: if (isLoaded) { $: if (isLoaded) {
@@ -94,52 +92,13 @@
wasAffiliationAvailable = isAffiliationAvailable; wasAffiliationAvailable = isAffiliationAvailable;
} }
$: filteredCharacters = characters.filter(char => { function handleCharacterSelect(event: CustomEvent) {
const searchTerm = searchInput.toLowerCase(); const character = event.detail;
const nameMatches = char.name.toLowerCase().includes(searchTerm); selectCharacter(character);
let epithetsMatches = false;
if (char.epithets) {
try {
const parsedEpithets = typeof char.epithets === 'string'
? JSON.parse(char.epithets)
: char.epithets;
if (Array.isArray(parsedEpithets)) {
epithetsMatches = parsedEpithets.some((epithet: string) =>
epithet.toLowerCase().includes(searchTerm)
);
} else if (typeof parsedEpithets === 'string') {
epithetsMatches = parsedEpithets.toLowerCase().includes(searchTerm);
}
} catch {
epithetsMatches = String(char.epithets).toLowerCase().includes(searchTerm);
}
}
return (nameMatches || epithetsMatches) &&
!selectedCharacters.some(selected => selected.id === char.id);
});
// Reset highlighted index when filtered list changes
$: if (filteredCharacters) {
highlightedIndex = 0;
}
// Scroll highlighted item into view
$: if (dropdownContainer && highlightedIndex >= 0) {
const highlightedButton = dropdownContainer.querySelector(
`button:nth-child(${highlightedIndex + 1})`
) as HTMLElement;
if (highlightedButton) {
highlightedButton.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
} }
function selectCharacter(character: any) { function selectCharacter(character: any) {
selectedCharacters = [character, ...selectedCharacters]; selectedCharacters = [character, ...selectedCharacters];
searchInput = '';
highlightedIndex = 0;
// Check if player won // Check if player won
if (character.id === dailyCharacter.id) { if (character.id === dailyCharacter.id) {
@@ -165,55 +124,11 @@
selectedCharacters = []; selectedCharacters = [];
localStorage.removeItem('dailyCharacterHistory'); localStorage.removeItem('dailyCharacterHistory');
} }
function handleKeydown(event: KeyboardEvent) {
if (filteredCharacters.length === 0) return;
switch (event.key) {
case 'ArrowDown':
event.preventDefault();
highlightedIndex = Math.min(highlightedIndex + 1, filteredCharacters.length - 1);
break;
case 'ArrowUp':
event.preventDefault();
highlightedIndex = Math.max(highlightedIndex - 1, 0);
break;
case 'Enter':
event.preventDefault();
if (filteredCharacters[highlightedIndex]) {
selectCharacter(filteredCharacters[highlightedIndex]);
}
break;
}
}
function submitGuess() {
if (filteredCharacters.length === 0) return;
const characterToSelect =
filteredCharacters[highlightedIndex] ?? filteredCharacters[0];
if (characterToSelect) {
selectCharacter(characterToSelect);
}
}
</script> </script>
<svelte:head> <svelte:head>
<title>OnePieceDle - Mode du jour</title> <title>OnePieceDle - Mode du jour</title>
<style> <style>
@keyframes hint-unlock {
0% {
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
}
50% {
box-shadow: 0 0 20px rgba(251, 146, 60, 0.8);
}
100% {
box-shadow: 0 0 0 rgba(251, 146, 60, 0);
}
}
.hint-unlocking {
animation: hint-unlock 0.6s ease-out;
}
@keyframes shadow-pulse { @keyframes shadow-pulse {
0% { 0% {
text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8); text-shadow: 0 0 20px rgba(0, 0, 0, 0.5), 0 0 40px rgba(50, 50, 50, 0.8);
@@ -307,460 +222,36 @@
<section class="mt-10 grid gap-6"> <section class="mt-10 grid gap-6">
{#if selectedCharacters.length > 0 && !hasWon} {#if selectedCharacters.length > 0 && !hasWon}
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"> <HintsPanel
<div class="grid gap-3 sm:grid-cols-3"> {dailyCharacter}
<button {selectedCharacters}
type="button" {showOriginUnlock}
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isOriginAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showOriginUnlock ? 'hint-unlocking' : ''}" {showFruitUnlock}
disabled={!isOriginAvailable} {showAffiliationUnlock}
onclick={() => showHintOrigin = !showHintOrigin} />
>
<p class="text-sm font-medium text-amber-100">Origine</p>
{#if showHintOrigin}
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.origin || 'Inconnue'}</p>
{:else if Math.max(0, 5 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 5 - selectedCharacters.length)} essais avant déblocage</p>
{:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if}
</button>
<button
type="button"
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isFruitAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showFruitUnlock ? 'hint-unlocking' : ''}"
disabled={!isFruitAvailable}
onclick={() => showHintFruit = !showHintFruit}
>
<p class="text-sm font-medium text-amber-100">Fruit du démon</p>
{#if showHintFruit}
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.devilFruitName || 'Aucun'}</p>
{:else if Math.max(0, 10 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 10 - selectedCharacters.length)} essais avant déblocage</p>
{:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if}
</button>
<button
type="button"
class="rounded-2xl border border-white/10 px-3 py-3 flex flex-col items-center justify-center cursor-pointer hover:bg-slate-900/80 transition-colors {isAffiliationAvailable ? 'bg-slate-950/60' : 'bg-slate-950/30 opacity-50 cursor-not-allowed hover:bg-slate-950/30'} {showAffiliationUnlock ? 'hint-unlocking' : ''}"
disabled={!isAffiliationAvailable}
onclick={() => showHintAffiliation = !showHintAffiliation}
>
<p class="text-sm font-medium text-amber-100">Affiliation</p>
{#if showHintAffiliation}
{@const affiliations = typeof dailyCharacter.affiliations === 'string'
? ((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 || 'Inconnue'}</p>
{:else if Math.max(0, 15 - selectedCharacters.length) > 0}
<p class="mt-2 text-xs text-slate-400">{Math.max(0, 15 - selectedCharacters.length)} essais avant déblocage</p>
{:else}
<p class="mt-2 text-xs text-slate-400">Indice disponible !</p>
{/if}
</button>
</div>
</div>
{/if} {/if}
{#if hasWon} {#if hasWon}
{#if isGeckoMoriaWin} <WinPanel
<div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect"> {dailyCharacter}
<div class="text-center"> {selectedCharacters}
<div class="text-3xl mb-2">🌑</div> {isGeckoMoriaWin}
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<div class="mt-3">
{#if dailyCharacter.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="inline-block"
>
<img
src={dailyCharacter.pictureUrl}
alt={dailyCharacter.name}
class="w-20 h-20 mx-auto rounded-full border-2 border-slate-600 shadow-lg object-cover hover:border-slate-500 transition-colors cursor-pointer opacity-80"
/>
</a>
{/if}
<p class="mt-2 text-lg font-bold text-slate-200">{dailyCharacter.name}</p>
</div>
</div>
</div>
{:else}
<div class="rounded-3xl border border-emerald-500/50 bg-emerald-500/10 p-4 shadow-[0_24px_60px_rgba(16,185,129,0.3)] backdrop-blur">
<div class="text-center">
<div class="text-3xl mb-2">🎉</div>
<h2 class="text-xl font-bold text-emerald-400 mb-1">Félicitations !</h2>
<p class="text-sm text-emerald-300">Vous avez trouvé le personnage en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
<div class="mt-3">
{#if dailyCharacter.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="inline-block"
>
<img
src={dailyCharacter.pictureUrl}
alt={dailyCharacter.name}
class="w-20 h-20 mx-auto rounded-full border-2 border-emerald-400 shadow-lg object-cover hover:border-emerald-300 transition-colors cursor-pointer"
/>
</a>
{/if}
<p class="mt-2 text-lg font-bold text-white">{dailyCharacter.name}</p>
</div>
</div>
</div>
{/if}
{:else}
<div class="rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur z-10">
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Entrer une supposition</h2>
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
<div class="relative w-full">
<input
bind:value={searchInput}
class="w-full rounded-full border border-amber-200/30 bg-slate-900/60 px-5 py-3 text-sm text-slate-100 placeholder:text-slate-400 focus:border-amber-200/70 focus:outline-none"
placeholder="Nom du personnage"
type="text"
onkeydown={handleKeydown}
/>
{#if searchInput.length > 0 && filteredCharacters.length > 0}
<div bind:this={dropdownContainer} class="absolute top-full left-0 right-0 mt-2 max-h-96 overflow-y-auto rounded-2xl border border-amber-200/30 bg-slate-900/90 backdrop-blur z-10">
{#each filteredCharacters as character, index (character.id)}
<button
class="w-full px-5 py-4 text-left text-base text-slate-100 transition border-b border-slate-800/50 last:border-b-0 flex items-center gap-4 {index === highlightedIndex ? 'bg-slate-700' : 'hover:bg-slate-800/70'}"
type="button"
onmouseenter={() => highlightedIndex = index}
onclick={() => selectCharacter(character)}
>
{#if character.pictureUrl}
<img
src={character.pictureUrl}
alt={character.name}
class="w-12 h-12 rounded-full object-cover border border-amber-200/30"
/> />
{:else} {:else}
<div class="w-12 h-12 rounded-full bg-slate-800 border border-amber-200/30 flex items-center justify-center"> <CharacterSearchInput
<span class="text-xs text-slate-400">?</span> {characters}
</div> {selectedCharacters}
{/if} on:select={handleCharacterSelect}
<div class="flex-1"> />
<span class="font-semibold text-amber-100">{character.name}</span>
{#if character.epithets}
{@const parsedEpithets = typeof character.epithets === 'string'
? JSON.parse(character.epithets)
: character.epithets}
{#if Array.isArray(parsedEpithets) && parsedEpithets.length > 0}
<span class="ml-2 text-xs text-slate-400">
{parsedEpithets.join(', ')}
</span>
{/if}
{/if}
</div>
</button>
{/each}
</div>
{/if}
</div>
<button
type="button"
onclick={submitGuess}
disabled={filteredCharacters.length === 0}
class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200 disabled:cursor-not-allowed disabled:opacity-50"
>
Valider
</button>
</div>
</div>
{/if} {/if}
</section> </section>
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"> <GuessHistoryTable
<div class="flex flex-col gap-4"> {selectedCharacters}
<div class="flex flex-col items-center gap-4 text-center"> {dailyCharacter}
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p> {columnVisibility}
</div>
{#if selectedCharacters.length === 0}
<p class="text-sm text-slate-200 text-center">Aucune tentative pour le moment.</p>
{:else}
<div class="overflow-x-auto pb-2 -mx-6 px-6 sm:mx-0 sm:px-0">
<div class="w-max min-w-max mx-auto">
<!-- Header -->
<div class="flex gap-2 mb-2">
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Personnage</p>
</div>
{#if columnVisibility.status !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Statut</p>
</div>
{/if}
{#if columnVisibility.gender !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Genre</p>
</div>
{/if}
{#if columnVisibility.affiliations !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Affiliations</p>
</div>
{/if}
{#if columnVisibility.devilFruitType !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Fruit</p>
</div>
{/if}
{#if columnVisibility.haki !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Haki</p>
</div>
{/if}
{#if columnVisibility.bounty !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Prime</p>
</div>
{/if}
{#if columnVisibility.height !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Taille</p>
</div>
{/if}
{#if columnVisibility.origin !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Origine</p>
</div>
{/if}
{#if columnVisibility.arc !== false}
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Arc</p>
</div>
{/if}
</div>
<!-- Rows -->
{#each selectedCharacters as character (character.id)}
<div class="flex gap-2 mb-2">
<!-- Personnage -->
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 bg-slate-950/60 overflow-hidden">
{#if character.pictureUrl}
<a
href={"https://onepiece.fandom.com/fr/wiki/" + character.url}
target="_blank"
rel="noopener noreferrer"
class="block w-full h-full"
>
<img
src={character.pictureUrl}
alt={character.name}
class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
/> />
</a>
{:else}
<div class="w-full h-full bg-slate-800 flex items-center justify-center p-2">
<span class="text-xl text-center font-semibold line-clamp-3">{character.name}</span>
</div>
{/if}
</div>
<!-- Vivant / Mort --> <YesterdayCharacter {yesterdayCharacter} />
{#if columnVisibility.status !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.status === dailyCharacter.status ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
<p class="text-sm font-bold text-white text-center">
{character.status === 'Alive' ? 'Vivant' : character.status === 'Deceased' || character.status === 'Dead' ? 'Mort' : character.status || 'Inconnu'}
</p>
</div>
{/if}
<!-- Genre -->
{#if columnVisibility.gender !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.gender === dailyCharacter.gender ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
<p class="text-base font-bold text-white text-center">
{character.gender === 'Male' ? 'Homme' : character.gender === 'Female' ? 'Femme' : character.gender || 'Inconnu'}
</p>
</div>
{/if}
<!-- Affiliations -->
{#if columnVisibility.affiliations !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
try {
const charAff = typeof character.affiliations === 'string'
? ((character.affiliations as string).includes('[') ? JSON.parse(character.affiliations) : (character.affiliations as string).split(',').map((a: string) => a.trim()))
: character.affiliations;
const dailyAff = typeof dailyCharacter.affiliations === 'string'
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
: dailyCharacter.affiliations;
const charFirstAff = Array.isArray(charAff) ? charAff[0] : charAff;
const dailyFirstAff = Array.isArray(dailyAff) ? dailyAff[0] : dailyAff;
return charFirstAff && dailyFirstAff && charFirstAff === dailyFirstAff ? 'bg-emerald-600/90' : 'bg-red-900/60';
} catch (e) {
return 'bg-slate-950/60';
}
})()} p-2 flex items-center justify-center overflow-hidden">
{#if character.affiliations}
{@const parsedAffiliations = typeof character.affiliations === 'string'
? (character.affiliations.includes('[') ? JSON.parse(character.affiliations) : character.affiliations.split(',').map((a: string) => a.trim()))
: character.affiliations}
{#if Array.isArray(parsedAffiliations) && parsedAffiliations.length > 0}
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations[0]}</p>
{:else}
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations}</p>
{/if}
{:else}
<p class="text-base font-bold text-slate-400 text-center">-</p>
{/if}
</div>
{/if}
<!-- Fruit -->
{#if columnVisibility.devilFruitType !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.devilFruitType === dailyCharacter.devilFruitType ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
{#if character.devilFruitType}
<p class="text-sm font-bold text-white text-center">{character.devilFruitType}</p>
{:else}
<p class="text-5xl font-bold text-white text-center"></p>
{/if}
</div>
{/if}
<!-- Haki -->
{#if columnVisibility.haki !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
if (character.hakiObservation === dailyCharacter.hakiObservation && character.hakiArmament === dailyCharacter.hakiArmament && character.hakiConqueror === dailyCharacter.hakiConqueror) {
return 'bg-emerald-600/90';
} else if ((character.hakiObservation && dailyCharacter.hakiObservation) ||
(character.hakiArmament && dailyCharacter.hakiArmament) ||
(character.hakiConqueror && dailyCharacter.hakiConqueror)) {
return 'bg-yellow-600/80';
} else {
return 'bg-red-900/60';
}
})()} p-2 flex items-center justify-center">
<p class="text-2xl font-bold text-white text-center">
{#if character.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
{#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
{#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
<span class="text-5xl"></span>
{/if}
</p>
</div>
{/if}
<!-- Prime -->
{#if columnVisibility.bounty !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.bounty === dailyCharacter.bounty ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
{#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
background-color: rgb(203, 213, 225);
clip-path: {character.bounty > dailyCharacter.bounty
? '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.bounty != null}
<p class="text-sm font-bold text-white text-center relative z-10">{formatBounty(character.bounty)} ฿</p>
{:else}
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
{/if}
</div>
{/if}
<!-- Taille -->
{#if columnVisibility.height !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.height === dailyCharacter.height ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
{#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
background-color: rgb(203, 213, 225);
clip-path: {character.height > dailyCharacter.height
? '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.height}
<p class="text-sm font-bold text-white text-center relative z-10">{character.height} m</p>
{:else}
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
{/if}
</div>
{/if}
<!-- Origine -->
{#if columnVisibility.origin !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.origin === dailyCharacter.origin ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
<p class="text-sm font-bold text-white text-center">{character.origin || 'Inconnue'}</p>
</div>
{/if}
<!-- Arc -->
{#if columnVisibility.arc !== false}
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.arcName === dailyCharacter.arcName ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
{#if character.arcName !== dailyCharacter.arcName && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
background-color: rgb(203, 213, 225);
clip-path: {character.firstAppearance > dailyCharacter.firstAppearance
? '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}
<p class="text-sm font-bold text-white text-center relative z-10">{character.arcName || 'Inconnu'}</p>
</div>
{/if}
</div>
{/each}
</div>
</div>
{/if}
</div>
</section>
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
{#if yesterdayCharacter}
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
{#if yesterdayCharacter.pictureUrl}
<img
src={yesterdayCharacter.pictureUrl}
alt={yesterdayCharacter.name}
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
/>
{:else}
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo
</div>
{/if}
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
{#if yesterdayCharacter.epithets}
<p class="mt-1 text-sm text-slate-400">
{typeof yesterdayCharacter.epithets === 'string'
? JSON.parse(yesterdayCharacter.epithets).join(', ')
: (yesterdayCharacter.epithets as string[]).join(', ')}
</p>
{/if}
</div>
<a
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
target="_blank"
rel="noopener noreferrer"
class="w-full rounded-full border border-amber-200/40 bg-transparent px-5 py-3 text-sm font-semibold text-amber-100 transition hover:border-amber-200 hover:text-amber-50 sm:w-auto"
>
Voir la page
</a>
</div>
{:else}
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
Photo
</div>
<div class="flex-1">
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
</div>
</div>
{/if}
</section>
</div> </div>
</main> </main>