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
All checks were successful
Build Docker Image / build (push) Successful in 1m12s
This commit is contained in:
@@ -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()
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user