refactor: improve bounty extraction logic and enhance character selection in infinite mode
This commit is contained in:
@@ -2,13 +2,16 @@ import { db } from '$lib/server/db';
|
||||
import { character, devilFruit, arc, user } from '$lib/server/db/schema';
|
||||
import { getOrCreateTodayCharacter, getTodayCharacterWinsCount } from '$lib/server/daily-character';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { count, eq } from 'drizzle-orm';
|
||||
|
||||
export const load: PageServerLoad = async () => {
|
||||
const [characters, devilFruits, arcs, users] = await Promise.all([
|
||||
db.select().from(character),
|
||||
db.select().from(devilFruit),
|
||||
db.select().from(arc),
|
||||
db.select().from(user)
|
||||
const [totalCharacters, totalDevilFruits, totalArcs, totalUsers, adminUsers, charactersInDaily] = await Promise.all([
|
||||
db.select({ count: count() }).from(character),
|
||||
db.select({ count: count() }).from(devilFruit),
|
||||
db.select({ count: count() }).from(arc),
|
||||
db.select({ count: count() }).from(user),
|
||||
db.select({ count: count() }).from(user).where(eq(user.isAdmin, true)),
|
||||
db.select({ count: count() }).from(character).where(eq(character.isInDailyMode, true))
|
||||
]);
|
||||
|
||||
// Get today's daily character and count wins
|
||||
@@ -21,12 +24,12 @@ export const load: PageServerLoad = async () => {
|
||||
|
||||
return {
|
||||
stats: {
|
||||
totalCharacters: characters.length,
|
||||
charactersInDaily: characters.filter((c) => c.isInDailyMode).length,
|
||||
totalDevilFruits: devilFruits.length,
|
||||
totalArcs: arcs.length,
|
||||
totalUsers: users.length,
|
||||
adminUsers: users.filter((u) => u.isAdmin).length,
|
||||
totalCharacters: totalCharacters[0].count,
|
||||
charactersInDaily: charactersInDaily[0].count,
|
||||
totalDevilFruits: totalDevilFruits[0].count,
|
||||
totalArcs: totalArcs[0].count,
|
||||
totalUsers: totalUsers[0].count,
|
||||
adminUsers: adminUsers[0].count,
|
||||
dailyCharacterWins
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getAllCharacters } from '$lib/server/daily-character';
|
||||
import { like } from 'drizzle-orm';
|
||||
|
||||
export async function load() {
|
||||
let characters = await getAllCharacters();
|
||||
const characters = await getAllCharacters();
|
||||
|
||||
// Load column visibility config
|
||||
const columnConfig = await db
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user