feat: implement character filters for gender, status, haki, devil fruit, height, and origin
All checks were successful
Build Docker Image / build (push) Successful in 1m12s

This commit is contained in:
2026-03-05 23:09:49 +01:00
parent 5978963939
commit c268cd5301
2 changed files with 262 additions and 19 deletions

View File

@@ -6,15 +6,6 @@ import { like } from 'drizzle-orm';
export async function load() { export async function load() {
let characters = await getAllCharacters(); let characters = await getAllCharacters();
// Filter out characters that have no height data
characters = characters.filter(char => char.height !== null);
// Filter out characters that have no status data
characters = characters.filter(char => char.status !== null);
// Filter out characters that have no origin data
characters = characters.filter(char => char.origin !== null);
// Load column visibility config // Load column visibility config
const columnConfig = await db const columnConfig = await db
.select() .select()

View File

@@ -24,6 +24,16 @@
arc: 'Arc' arc: 'Arc'
}; };
// Character filters
let characterFilters = {
gender: [] as string[],
hasHaki: false,
hasDevilFruit: null as boolean | null, // null = all, true = with fruit, false = without fruit
status: [] as string[],
hasHeight: false,
hasOrigin: false
};
let wasOriginAvailable = false; let wasOriginAvailable = false;
let wasFruitAvailable = false; let wasFruitAvailable = false;
let wasAffiliationAvailable = false; let wasAffiliationAvailable = false;
@@ -51,6 +61,16 @@
columnVisibility = data.columnVisibility || {}; columnVisibility = data.columnVisibility || {};
} }
// Load character filters from localStorage
const storedFilters = localStorage.getItem('infiniteCharacterFilters');
if (storedFilters) {
try {
characterFilters = JSON.parse(storedFilters);
} catch (e) {
console.error('Failed to parse filters', e);
}
}
// Load current character ID and history IDs from localStorage // Load current character ID and history IDs from localStorage
const storedCharacterId = localStorage.getItem('infiniteCurrentCharacterId'); const storedCharacterId = localStorage.getItem('infiniteCurrentCharacterId');
const storedHistoryIds = localStorage.getItem('infiniteSelectedCharacterIds'); const storedHistoryIds = localStorage.getItem('infiniteSelectedCharacterIds');
@@ -93,6 +113,11 @@
localStorage.setItem('infiniteColumnVisibility', JSON.stringify(columnVisibility)); localStorage.setItem('infiniteColumnVisibility', JSON.stringify(columnVisibility));
} }
// Save character filters to localStorage whenever they change
$: if (isLoaded) {
localStorage.setItem('infiniteCharacterFilters', JSON.stringify(characterFilters));
}
// Save current character ID to localStorage whenever it changes // Save current character ID to localStorage whenever it changes
$: if (isLoaded && currentCharacter) { $: if (isLoaded && currentCharacter) {
localStorage.setItem('infiniteCurrentCharacterId', JSON.stringify(currentCharacter.id)); localStorage.setItem('infiniteCurrentCharacterId', JSON.stringify(currentCharacter.id));
@@ -104,7 +129,45 @@
localStorage.setItem('infiniteSelectedCharacterIds', JSON.stringify(selectedIds)); localStorage.setItem('infiniteSelectedCharacterIds', JSON.stringify(selectedIds));
} }
$: characters = data.characters || []; $: allCharacters = data.characters || [];
// Filter characters based on selected filters
$: characters = allCharacters.filter((char: any) => {
// Gender filter
if (characterFilters.gender.length > 0 && !characterFilters.gender.includes(char.gender)) {
return false;
}
// Haki filter
if (characterFilters.hasHaki && !(char.hakiObservation || char.hakiArmament || char.hakiConqueror)) {
return false;
}
// Devil fruit filter
if (characterFilters.hasDevilFruit !== null) {
const hasDevil = char.devilFruitId !== null && char.devilFruitId !== undefined;
if (characterFilters.hasDevilFruit !== hasDevil) {
return false;
}
}
// Status filter
if (characterFilters.status.length > 0 && !characterFilters.status.includes(char.status)) {
return false;
}
// Height filter
if (characterFilters.hasHeight && (char.height === null || char.height === undefined)) {
return false;
}
// Origin filter
if (characterFilters.hasOrigin && (char.origin === null || char.origin === undefined || char.origin === '')) {
return false;
}
return true;
});
$: hasWon = currentCharacter && selectedCharacters.some(char => char.id === currentCharacter.id); $: hasWon = currentCharacter && selectedCharacters.some(char => char.id === currentCharacter.id);
$: if (hasWon && currentCharacter?.id === 'gecko_moria_gecko_moria') { $: if (hasWon && currentCharacter?.id === 'gecko_moria_gecko_moria') {
isGeckoMoriaWin = true; isGeckoMoriaWin = true;
@@ -184,6 +247,83 @@
// Add the current character as the correct answer // Add the current character as the correct answer
selectedCharacters = [currentCharacter, ...selectedCharacters]; selectedCharacters = [currentCharacter, ...selectedCharacters];
} }
function toggleGenderFilter(gender: string) {
if (characterFilters.gender.includes(gender)) {
characterFilters.gender = characterFilters.gender.filter(g => g !== gender);
} else {
characterFilters.gender = [...characterFilters.gender, gender];
}
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleStatusFilter(status: string) {
if (characterFilters.status.includes(status)) {
characterFilters.status = characterFilters.status.filter(s => s !== status);
} else {
characterFilters.status = [...characterFilters.status, status];
}
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleHakiFilter() {
characterFilters.hasHaki = !characterFilters.hasHaki;
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleDevilFruitFilter() {
if (characterFilters.hasDevilFruit === null) {
characterFilters.hasDevilFruit = true;
} else if (characterFilters.hasDevilFruit === true) {
characterFilters.hasDevilFruit = false;
} else {
characterFilters.hasDevilFruit = null;
}
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleHeightFilter() {
characterFilters.hasHeight = !characterFilters.hasHeight;
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function toggleOriginFilter() {
characterFilters.hasOrigin = !characterFilters.hasOrigin;
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
function clearAllFilters() {
characterFilters = {
gender: [],
hasHaki: false,
hasDevilFruit: null,
status: [],
hasHeight: false,
hasOrigin: false
};
// Regenerate character with new filters
if (!hasWon) {
generateNewCharacter();
}
}
</script> </script>
<svelte:head> <svelte:head>
@@ -313,21 +453,21 @@
{showFruitUnlock} {showFruitUnlock}
{showAffiliationUnlock} {showAffiliationUnlock}
/> />
<div class="flex justify-center mt-2">
<button
type="button"
onclick={revealAnswer}
class="rounded-lg border border-red-600/40 bg-red-900/20 px-4 py-2 text-sm text-red-300 transition hover:border-red-500 hover:bg-red-900/40 hover:text-red-200"
>
Révéler la réponse
</button>
</div>
{/if} {/if}
<CharacterSearchInput <CharacterSearchInput
{characters} {characters}
{selectedCharacters} {selectedCharacters}
on:select={handleCharacterSelect} on:select={handleCharacterSelect}
/> />
<div class="flex justify-center">
<button
type="button"
onclick={revealAnswer}
class="rounded-full border border-red-500/40 bg-transparent px-5 py-2 text-sm font-semibold text-red-300 transition hover:border-red-500 hover:bg-red-900/20"
>
Révéler la réponse
</button>
</div>
{/if} {/if}
{:else} {: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">
@@ -343,6 +483,118 @@
{columnVisibility} {columnVisibility}
/> />
<!-- 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="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>
{#if characterFilters.gender.length > 0 || characterFilters.hasHaki || characterFilters.hasDevilFruit !== null || characterFilters.status.length > 0 || characterFilters.hasHeight || characterFilters.hasOrigin}
<button
type="button"
onclick={clearAllFilters}
class="text-xs text-red-300 hover:text-red-200 transition"
>
Réinitialiser
</button>
{/if}
</div>
<div class="space-y-3">
<!-- Gender Filter -->
<div>
<p class="text-xs text-slate-400 mb-2">Genre</p>
<div class="flex flex-wrap gap-2">
{#each ['Male', 'Female'] as 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)
? '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'}"
>
{gender === 'Male' ? 'Homme' : 'Femme'}
</button>
{/each}
</div>
</div>
<!-- Status Filter -->
<div>
<p class="text-xs text-slate-400 mb-2">Statut</p>
<div class="flex flex-wrap gap-2">
{#each ['Alive', 'Dead', 'Unknown'] as 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)
? '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'}"
>
{status === 'Alive' ? 'Vivant' : status === 'Dead' ? 'Mort' : 'Inconnu'}
</button>
{/each}
</div>
</div>
<!-- Haki Filter -->
<div>
<p class="text-xs text-slate-400 mb-2">Capacités</p>
<div class="flex flex-wrap gap-2">
<button
type="button"
onclick={toggleHakiFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasHaki
? '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'}"
>
A du Haki
</button>
<button
type="button"
onclick={toggleDevilFruitFilter}
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'}"
>
{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>
<div class="flex flex-wrap gap-2">
<button
type="button"
onclick={toggleHeightFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasHeight
? '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'}"
>
Taille définie
</button>
<button
type="button"
onclick={toggleOriginFilter}
class="rounded-full border px-2.5 py-1 text-xs font-medium transition-colors {characterFilters.hasOrigin
? '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'}"
>
Origine définie
</button>
</div>
</div>
<p class="text-xs text-slate-500 mt-2">
{characters.length} personnage{characters.length > 1 ? 's' : ''} disponible{characters.length > 1 ? 's' : ''}
</p>
</div>
</div>
</section>
<!-- Column Visibility Toggle --> <!-- Column Visibility Toggle -->
<section class="mt-6"> <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 sm:p-4 backdrop-blur">