|
|
|
|
@@ -1,16 +1,22 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
import { onMount } from 'svelte';
|
|
|
|
|
import { onDestroy, onMount } from 'svelte';
|
|
|
|
|
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
|
|
|
|
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
|
|
|
|
|
import WinPanel from '$lib/components/WinPanel.svelte';
|
|
|
|
|
import HintsPanel from '$lib/components/HintsPanel.svelte';
|
|
|
|
|
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
|
|
|
|
|
|
|
|
|
export let data;
|
|
|
|
|
|
|
|
|
|
let selectedCharacters: any[] = [];
|
|
|
|
|
let currentCharacter: any = null;
|
|
|
|
|
let selectedCharacters: CharacterWithRelations[] = [];
|
|
|
|
|
let currentCharacter: CharacterWithRelations | null = null;
|
|
|
|
|
let isLoaded = false;
|
|
|
|
|
let score = 0;
|
|
|
|
|
type ArcFilterOption = { id: string; name: string };
|
|
|
|
|
let allCharacters: CharacterWithRelations[] = [];
|
|
|
|
|
let characters: CharacterWithRelations[] = [];
|
|
|
|
|
let availableArcs: ArcFilterOption[] = [];
|
|
|
|
|
let hasWon = false;
|
|
|
|
|
let columnVisibility: Record<string, boolean> = {};
|
|
|
|
|
const columnDisplayNames: Record<string, string> = {
|
|
|
|
|
status: 'Statut',
|
|
|
|
|
@@ -35,13 +41,85 @@
|
|
|
|
|
arcs: [] as string[]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let wasOriginAvailable = false;
|
|
|
|
|
let wasFruitAvailable = false;
|
|
|
|
|
let wasAffiliationAvailable = false;
|
|
|
|
|
let showOriginUnlock = false;
|
|
|
|
|
let showFruitUnlock = false;
|
|
|
|
|
let showAffiliationUnlock = false;
|
|
|
|
|
let isGeckoMoriaWin = false;
|
|
|
|
|
let originUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
let fruitUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
let affiliationUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
|
|
|
|
|
|
|
|
function clearUnlockTimeout(timeout: ReturnType<typeof setTimeout> | null) {
|
|
|
|
|
if (timeout) {
|
|
|
|
|
clearTimeout(timeout);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pulseUnlock(type: 'origin' | 'fruit' | 'affiliation') {
|
|
|
|
|
if (type === 'origin') {
|
|
|
|
|
clearUnlockTimeout(originUnlockTimeout);
|
|
|
|
|
showOriginUnlock = true;
|
|
|
|
|
originUnlockTimeout = setTimeout(() => {
|
|
|
|
|
showOriginUnlock = false;
|
|
|
|
|
originUnlockTimeout = null;
|
|
|
|
|
}, 600);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type === 'fruit') {
|
|
|
|
|
clearUnlockTimeout(fruitUnlockTimeout);
|
|
|
|
|
showFruitUnlock = true;
|
|
|
|
|
fruitUnlockTimeout = setTimeout(() => {
|
|
|
|
|
showFruitUnlock = false;
|
|
|
|
|
fruitUnlockTimeout = null;
|
|
|
|
|
}, 600);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
clearUnlockTimeout(affiliationUnlockTimeout);
|
|
|
|
|
showAffiliationUnlock = true;
|
|
|
|
|
affiliationUnlockTimeout = setTimeout(() => {
|
|
|
|
|
showAffiliationUnlock = false;
|
|
|
|
|
affiliationUnlockTimeout = null;
|
|
|
|
|
}, 600);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function syncHintAvailability(previousGuessCount: number, nextGuessCount: number, animateUnlocks = false) {
|
|
|
|
|
const nextOriginAvailable = nextGuessCount >= 5;
|
|
|
|
|
const nextFruitAvailable = nextGuessCount >= 10;
|
|
|
|
|
const nextAffiliationAvailable = nextGuessCount >= 15;
|
|
|
|
|
|
|
|
|
|
if (animateUnlocks && nextOriginAvailable && previousGuessCount < 5) {
|
|
|
|
|
pulseUnlock('origin');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (animateUnlocks && nextFruitAvailable && previousGuessCount < 10) {
|
|
|
|
|
pulseUnlock('fruit');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (animateUnlocks && nextAffiliationAvailable && previousGuessCount < 15) {
|
|
|
|
|
pulseUnlock('affiliation');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!nextOriginAvailable) {
|
|
|
|
|
showOriginUnlock = false;
|
|
|
|
|
clearUnlockTimeout(originUnlockTimeout);
|
|
|
|
|
originUnlockTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!nextFruitAvailable) {
|
|
|
|
|
showFruitUnlock = false;
|
|
|
|
|
clearUnlockTimeout(fruitUnlockTimeout);
|
|
|
|
|
fruitUnlockTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!nextAffiliationAvailable) {
|
|
|
|
|
showAffiliationUnlock = false;
|
|
|
|
|
clearUnlockTimeout(affiliationUnlockTimeout);
|
|
|
|
|
affiliationUnlockTimeout = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load from localStorage on mount
|
|
|
|
|
onMount(() => {
|
|
|
|
|
@@ -56,6 +134,7 @@
|
|
|
|
|
try {
|
|
|
|
|
columnVisibility = JSON.parse(storedColumnVisibility);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to parse column visibility', e);
|
|
|
|
|
columnVisibility = data.columnVisibility || {};
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
@@ -86,18 +165,19 @@
|
|
|
|
|
const historyIds = JSON.parse(storedHistoryIds);
|
|
|
|
|
|
|
|
|
|
// Find the character object by ID
|
|
|
|
|
currentCharacter = characters.find((c: any) => c.id === charId);
|
|
|
|
|
currentCharacter = characters.find((c: CharacterWithRelations) => c.id === charId) || null;
|
|
|
|
|
|
|
|
|
|
// Find all character objects by their IDs
|
|
|
|
|
selectedCharacters = historyIds
|
|
|
|
|
.map((id: string) => characters.find((c: any) => c.id === id))
|
|
|
|
|
.filter((c: any) => c !== undefined);
|
|
|
|
|
.map((id: string) => characters.find((c: CharacterWithRelations) => c.id === id))
|
|
|
|
|
.filter((c: CharacterWithRelations | undefined) => !!c) as CharacterWithRelations[];
|
|
|
|
|
|
|
|
|
|
// If character not found, generate a new one
|
|
|
|
|
if (!currentCharacter) {
|
|
|
|
|
generateNewCharacter();
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Failed to parse character data', e);
|
|
|
|
|
// If parsing fails, generate a new character
|
|
|
|
|
generateNewCharacter();
|
|
|
|
|
}
|
|
|
|
|
@@ -105,9 +185,16 @@
|
|
|
|
|
generateNewCharacter();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
syncHintAvailability(0, selectedCharacters.length);
|
|
|
|
|
isLoaded = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onDestroy(() => {
|
|
|
|
|
clearUnlockTimeout(originUnlockTimeout);
|
|
|
|
|
clearUnlockTimeout(fruitUnlockTimeout);
|
|
|
|
|
clearUnlockTimeout(affiliationUnlockTimeout);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Save score to localStorage whenever it changes
|
|
|
|
|
$: if (isLoaded) {
|
|
|
|
|
localStorage.setItem('infiniteScore', score.toString());
|
|
|
|
|
@@ -130,26 +217,30 @@
|
|
|
|
|
|
|
|
|
|
// Save selected character IDs to localStorage whenever it changes
|
|
|
|
|
$: if (isLoaded) {
|
|
|
|
|
const selectedIds = selectedCharacters.map((c: any) => c.id);
|
|
|
|
|
const selectedIds = selectedCharacters.map((c: CharacterWithRelations) => c.id);
|
|
|
|
|
localStorage.setItem('infiniteSelectedCharacterIds', JSON.stringify(selectedIds));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$: allCharacters = data.characters || [];
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Extract unique arcs from all characters
|
|
|
|
|
$: availableArcs = [
|
|
|
|
|
...new Map(
|
|
|
|
|
$: {
|
|
|
|
|
const arcMap = new Map<string, ArcFilterOption>(
|
|
|
|
|
allCharacters
|
|
|
|
|
.filter((char: any) => char.arcId && char.arcName)
|
|
|
|
|
.map((char: any) => [char.arcId, { id: char.arcId, name: char.arcName }])
|
|
|
|
|
).values()
|
|
|
|
|
]
|
|
|
|
|
.sort((a: any, b: any) => (a.name || '').localeCompare(b.name || ''));
|
|
|
|
|
.filter(
|
|
|
|
|
(char: CharacterWithRelations): char is CharacterWithRelations & { arcId: string; arcName: string } =>
|
|
|
|
|
typeof char.arcId === 'string' && char.arcId.length > 0 && typeof char.arcName === 'string' && char.arcName.length > 0
|
|
|
|
|
)
|
|
|
|
|
.map((char: CharacterWithRelations & { arcId: string; arcName: string }) => [char.arcId, { id: char.arcId, name: char.arcName }])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
availableArcs = [...arcMap.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter characters based on selected filters
|
|
|
|
|
$: characters = allCharacters.filter((char: any) => {
|
|
|
|
|
$: characters = allCharacters.filter((char: CharacterWithRelations) => {
|
|
|
|
|
// Gender filter
|
|
|
|
|
if (characterFilters.gender.length > 0 && !characterFilters.gender.includes(char.gender)) {
|
|
|
|
|
if (characterFilters.gender.length > 0 && (char.gender == null || !characterFilters.gender.includes(char.gender))) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -185,48 +276,26 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Arc filter
|
|
|
|
|
if (characterFilters.arcs.length > 0 && !characterFilters.arcs.includes(char.arcId)) {
|
|
|
|
|
if (characterFilters.arcs.length > 0 && (char.arcId == null || !characterFilters.arcs.includes(char.arcId))) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
$: hasWon = currentCharacter && selectedCharacters.some(char => char.id === currentCharacter.id);
|
|
|
|
|
$: {
|
|
|
|
|
const currentCharacterId = currentCharacter?.id;
|
|
|
|
|
hasWon = currentCharacterId != null && selectedCharacters.some(char => char.id === currentCharacterId);
|
|
|
|
|
}
|
|
|
|
|
$: if (hasWon && currentCharacter?.id === 'gecko_moria_gecko_moria') {
|
|
|
|
|
isGeckoMoriaWin = true;
|
|
|
|
|
} else if (!hasWon) {
|
|
|
|
|
isGeckoMoriaWin = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Hint availability tracking for unlock animations
|
|
|
|
|
$: isOriginAvailable = selectedCharacters.length >= 5;
|
|
|
|
|
$: isFruitAvailable = selectedCharacters.length >= 10;
|
|
|
|
|
$: isAffiliationAvailable = selectedCharacters.length >= 15;
|
|
|
|
|
|
|
|
|
|
// Track hint unlocks
|
|
|
|
|
$: if (isLoaded) {
|
|
|
|
|
if (isOriginAvailable && !wasOriginAvailable) {
|
|
|
|
|
showOriginUnlock = true;
|
|
|
|
|
setTimeout(() => (showOriginUnlock = false), 600);
|
|
|
|
|
}
|
|
|
|
|
wasOriginAvailable = isOriginAvailable;
|
|
|
|
|
|
|
|
|
|
if (isFruitAvailable && !wasFruitAvailable) {
|
|
|
|
|
showFruitUnlock = true;
|
|
|
|
|
setTimeout(() => (showFruitUnlock = false), 600);
|
|
|
|
|
}
|
|
|
|
|
wasFruitAvailable = isFruitAvailable;
|
|
|
|
|
|
|
|
|
|
if (isAffiliationAvailable && !wasAffiliationAvailable) {
|
|
|
|
|
showAffiliationUnlock = true;
|
|
|
|
|
setTimeout(() => (showAffiliationUnlock = false), 600);
|
|
|
|
|
}
|
|
|
|
|
wasAffiliationAvailable = isAffiliationAvailable;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateNewCharacter() {
|
|
|
|
|
if (characters.length === 0) return;
|
|
|
|
|
currentCharacter = characters[Math.floor(Math.random() * characters.length)];
|
|
|
|
|
syncHintAvailability(selectedCharacters.length, 0);
|
|
|
|
|
selectedCharacters = [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -235,11 +304,18 @@
|
|
|
|
|
selectCharacter(character);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function selectCharacter(character: any) {
|
|
|
|
|
function selectCharacter(character: CharacterWithRelations) {
|
|
|
|
|
const current = currentCharacter;
|
|
|
|
|
if (!current) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previousGuessCount = selectedCharacters.length;
|
|
|
|
|
selectedCharacters = [character, ...selectedCharacters];
|
|
|
|
|
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
|
|
|
|
|
|
|
|
|
|
// Check if player won
|
|
|
|
|
if (character.id === currentCharacter.id) {
|
|
|
|
|
if (character.id === current.id) {
|
|
|
|
|
// Increment score (saved to localStorage via reactive statement)
|
|
|
|
|
score++;
|
|
|
|
|
// Don't auto-generate next character - wait for user to click "Recommencer"
|
|
|
|
|
@@ -265,10 +341,16 @@
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function revealAnswer() {
|
|
|
|
|
if (!currentCharacter) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset score (strike)
|
|
|
|
|
score = 0;
|
|
|
|
|
// Add the current character as the correct answer
|
|
|
|
|
const previousGuessCount = selectedCharacters.length;
|
|
|
|
|
selectedCharacters = [currentCharacter, ...selectedCharacters];
|
|
|
|
|
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleGenderFilter(gender: string) {
|
|
|
|
|
@@ -396,16 +478,22 @@
|
|
|
|
|
<style>
|
|
|
|
|
@keyframes shadow-pulse {
|
|
|
|
|
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);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
50% {
|
|
|
|
|
text-shadow: 0 0 60px rgba(0, 0, 0, 0.9), 0 0 100px rgba(30, 30, 30, 1),
|
|
|
|
|
text-shadow:
|
|
|
|
|
0 0 60px rgba(0, 0, 0, 0.9),
|
|
|
|
|
0 0 100px rgba(30, 30, 30, 1),
|
|
|
|
|
inset 0 0 50px rgba(0, 0, 0, 0.7);
|
|
|
|
|
opacity: 0.9;
|
|
|
|
|
}
|
|
|
|
|
100% {
|
|
|
|
|
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);
|
|
|
|
|
opacity: 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -461,18 +549,22 @@
|
|
|
|
|
</svelte:head>
|
|
|
|
|
|
|
|
|
|
<main
|
|
|
|
|
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin ? 'moria-screen-chaos' : ''}"
|
|
|
|
|
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin
|
|
|
|
|
? 'moria-screen-chaos'
|
|
|
|
|
: ''}"
|
|
|
|
|
>
|
|
|
|
|
<div class="absolute inset-0 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
|
|
|
|
|
<div
|
|
|
|
|
class="absolute inset-0 mix-blend-screen opacity-20 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)]"
|
|
|
|
|
class="absolute inset-0 bg-linear-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"
|
|
|
|
|
></div>
|
|
|
|
|
<div
|
|
|
|
|
class="absolute inset-0 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)] opacity-20 mix-blend-screen"
|
|
|
|
|
></div>
|
|
|
|
|
|
|
|
|
|
<div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-8 sm:py-10">
|
|
|
|
|
<header class="flex flex-col items-start gap-6 w-full">
|
|
|
|
|
<header class="flex w-full flex-col items-start gap-6">
|
|
|
|
|
<div class="flex w-full items-center justify-between gap-4">
|
|
|
|
|
<div>
|
|
|
|
|
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 sm:text-5xl">
|
|
|
|
|
<h1 class="text-3xl font-black tracking-[0.25em] text-amber-50 uppercase sm:text-5xl">
|
|
|
|
|
Mode Infini
|
|
|
|
|
</h1>
|
|
|
|
|
<p class="mt-2 text-2xl font-bold text-amber-300">Score: {score}</p>
|
|
|
|
|
@@ -496,11 +588,7 @@
|
|
|
|
|
{#if currentCharacter}
|
|
|
|
|
{#if hasWon}
|
|
|
|
|
<div>
|
|
|
|
|
<WinPanel
|
|
|
|
|
selectedCharacter={currentCharacter}
|
|
|
|
|
{selectedCharacters}
|
|
|
|
|
{isGeckoMoriaWin}
|
|
|
|
|
/>
|
|
|
|
|
<WinPanel selectedCharacter={currentCharacter} {selectedCharacters} {isGeckoMoriaWin} />
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={nextCharacter}
|
|
|
|
|
@@ -518,7 +606,7 @@
|
|
|
|
|
{showFruitUnlock}
|
|
|
|
|
{showAffiliationUnlock}
|
|
|
|
|
/>
|
|
|
|
|
<div class="flex justify-center mt-2">
|
|
|
|
|
<div class="mt-2 flex justify-center">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={revealAnswer}
|
|
|
|
|
@@ -535,7 +623,9 @@
|
|
|
|
|
/>
|
|
|
|
|
{/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">
|
|
|
|
|
<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"
|
|
|
|
|
>
|
|
|
|
|
<p class="text-center text-slate-300">Chargement du personnage...</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
@@ -550,30 +640,34 @@
|
|
|
|
|
|
|
|
|
|
<!-- Character Filters -->
|
|
|
|
|
<section class="mt-6">
|
|
|
|
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 sm:p-4 backdrop-blur">
|
|
|
|
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4">
|
|
|
|
|
<div class="mb-3 flex items-center justify-between gap-3">
|
|
|
|
|
<h3 class="text-xs font-semibold uppercase tracking-[0.2em] text-amber-200">Filtres de personnages</h3>
|
|
|
|
|
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
|
|
|
|
|
Filtres de personnages
|
|
|
|
|
</h3>
|
|
|
|
|
{#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasOrigin || characterFilters.arcs.length > 0}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={clearAllFilters}
|
|
|
|
|
class="text-xs text-red-300 hover:text-red-200 transition"
|
|
|
|
|
class="text-xs text-red-300 transition hover:text-red-200"
|
|
|
|
|
>
|
|
|
|
|
Réinitialiser
|
|
|
|
|
</button>
|
|
|
|
|
{/if}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
<!-- Gender Filter -->
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs text-slate-400 mb-2">Genre</p>
|
|
|
|
|
<p class="mb-2 text-xs text-slate-400">Genre</p>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
{#each ['Male', 'Female'] as gender}
|
|
|
|
|
{#each ['Male', 'Female'] as gender (gender)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => toggleGenderFilter(gender)}
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.gender.includes(gender)
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.gender.includes(
|
|
|
|
|
gender
|
|
|
|
|
)
|
|
|
|
|
? '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'}"
|
|
|
|
|
>
|
|
|
|
|
@@ -585,13 +679,15 @@
|
|
|
|
|
|
|
|
|
|
<!-- Status Filter -->
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs text-slate-400 mb-2">Statut</p>
|
|
|
|
|
<p class="mb-2 text-xs text-slate-400">Statut</p>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
{#each ['Alive', 'Dead', 'Unknown'] as status}
|
|
|
|
|
{#each ['Alive', 'Dead', 'Unknown'] as status (status)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => toggleStatusFilter(status)}
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.status.includes(status)
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.status.includes(
|
|
|
|
|
status
|
|
|
|
|
)
|
|
|
|
|
? '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'}"
|
|
|
|
|
>
|
|
|
|
|
@@ -603,7 +699,7 @@
|
|
|
|
|
|
|
|
|
|
<!-- Haki Filter -->
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs text-slate-400 mb-2">Capacités</p>
|
|
|
|
|
<p class="mb-2 text-xs text-slate-400">Capacités</p>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@@ -617,20 +713,25 @@
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={toggleDevilFruitFilter}
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasDevilFruit === true
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasDevilFruit ===
|
|
|
|
|
true
|
|
|
|
|
? 'border-amber-300/50 bg-amber-300/10 text-amber-100 hover:bg-amber-300/20'
|
|
|
|
|
: characterFilters.hasDevilFruit === false
|
|
|
|
|
? 'border-purple-300/50 bg-purple-300/10 text-purple-100 hover:bg-purple-300/20'
|
|
|
|
|
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
|
|
|
|
|
? 'border-purple-300/50 bg-purple-300/10 text-purple-100 hover:bg-purple-300/20'
|
|
|
|
|
: 'border-white/20 bg-slate-900/40 text-slate-400 hover:bg-slate-900/60'}"
|
|
|
|
|
>
|
|
|
|
|
{characterFilters.hasDevilFruit === null ? 'Fruit (Tous)' : characterFilters.hasDevilFruit ? 'Avec Fruit' : 'Sans Fruit'}
|
|
|
|
|
{characterFilters.hasDevilFruit === null
|
|
|
|
|
? 'Fruit (Tous)'
|
|
|
|
|
: characterFilters.hasDevilFruit
|
|
|
|
|
? 'Avec Fruit'
|
|
|
|
|
: 'Sans Fruit'}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- Informations Filter -->
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs text-slate-400 mb-2">Informations</p>
|
|
|
|
|
<p class="mb-2 text-xs text-slate-400">Informations</p>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
@@ -652,16 +753,18 @@
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Arc Filter -->
|
|
|
|
|
<div>
|
|
|
|
|
<p class="text-xs text-slate-400 mb-2">Arcs</p>
|
|
|
|
|
<p class="mb-2 text-xs text-slate-400">Arcs</p>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
{#each availableArcs as arc (arc.id)}
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onclick={() => toggleArcFilter(arc.id)}
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.arcs.includes(arc.id)
|
|
|
|
|
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.arcs.includes(
|
|
|
|
|
arc.id
|
|
|
|
|
)
|
|
|
|
|
? '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'}"
|
|
|
|
|
>
|
|
|
|
|
@@ -670,9 +773,12 @@
|
|
|
|
|
{/each}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p class="text-xs text-slate-500 mt-2">
|
|
|
|
|
{characters.length} personnage{characters.length > 1 ? 's' : ''} disponible{characters.length > 1 ? 's' : ''}
|
|
|
|
|
|
|
|
|
|
<p class="mt-2 text-xs text-slate-500">
|
|
|
|
|
{characters.length} personnage{characters.length > 1 ? 's' : ''} disponible{characters.length >
|
|
|
|
|
1
|
|
|
|
|
? 's'
|
|
|
|
|
: ''}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
@@ -680,11 +786,15 @@
|
|
|
|
|
|
|
|
|
|
<!-- Column Visibility Toggle -->
|
|
|
|
|
<section class="mt-6">
|
|
|
|
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 sm:p-4 backdrop-blur">
|
|
|
|
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-3 backdrop-blur sm:p-4">
|
|
|
|
|
<div class="mb-3 flex items-center justify-between gap-3">
|
|
|
|
|
<h3 class="text-xs font-semibold uppercase tracking-[0.2em] text-amber-200">Colonnes</h3>
|
|
|
|
|
<h3 class="text-xs font-semibold tracking-[0.2em] text-amber-200 uppercase">
|
|
|
|
|
Colonnes
|
|
|
|
|
</h3>
|
|
|
|
|
<p class="text-xs text-slate-400">
|
|
|
|
|
{Object.values(columnVisibility).filter(Boolean).length}/{Object.keys(columnVisibility).length}
|
|
|
|
|
{Object.values(columnVisibility).filter(Boolean).length}/{Object.keys(
|
|
|
|
|
columnVisibility
|
|
|
|
|
).length}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
|
|
|
|