feat: implement infinite mode with character selection and scoring; refactor daily character storage logic
This commit is contained in:
@@ -168,6 +168,17 @@ export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]
|
|||||||
return applyCharacterOverrides(characters);
|
return applyCharacterOverrides(characters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllCharacters(): Promise<CharacterWithRelations[]> {
|
||||||
|
const characters = (await db
|
||||||
|
.select(characterWithRelationsSelect)
|
||||||
|
.from(character)
|
||||||
|
.leftJoin(arc, eq(character.arcId, arc.id))
|
||||||
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
|
.all()) as CharacterWithRelations[];
|
||||||
|
|
||||||
|
return applyCharacterOverrides(characters);
|
||||||
|
}
|
||||||
|
|
||||||
export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> {
|
export async function getCharacterById(characterId: string): Promise<CharacterWithRelations | null> {
|
||||||
const [found] = await db
|
const [found] = await db
|
||||||
.select(characterWithRelationsSelect)
|
.select(characterWithRelationsSelect)
|
||||||
|
|||||||
@@ -37,12 +37,15 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
<div class="rounded-2xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||||
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Partie libre</h2>
|
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Mode Infini</h2>
|
||||||
<p class="mt-3 text-lg font-semibold text-white">Entraine-toi avec des pirates legendaires</p>
|
<p class="mt-3 text-lg font-semibold text-white">Des defis sans fin</p>
|
||||||
<p class="mt-2 text-sm text-slate-200">Choisis une epoque, regle la difficulte et vogue a ton rythme.</p>
|
<p class="mt-2 text-sm text-slate-200">Enchaine les personnages et croise ton score. Pas de limite, que du plaisir.</p>
|
||||||
<button class="mt-5 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">
|
<a
|
||||||
En construction
|
href="/infinite"
|
||||||
</button>
|
class="mt-5 inline-flex w-full items-center justify-center 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"
|
||||||
|
>
|
||||||
|
Jouer
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
|
<div class="w-full rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur sm:p-8">
|
||||||
|
|||||||
@@ -21,11 +21,11 @@
|
|||||||
|
|
||||||
// Load from localStorage on mount
|
// Load from localStorage on mount
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const storedDailyCharacterId = localStorage.getItem('currentDailyCharacterId');
|
const storedDailyCharacterId = localStorage.getItem('dailyCurrentCharacterId');
|
||||||
const currentDailyCharacterId = dailyCharacter?.id;
|
const dailyCurrentCharacterId = dailyCharacter?.id;
|
||||||
|
|
||||||
// If the daily character has changed, clear the history
|
// If the daily character has changed, clear the history
|
||||||
if (storedDailyCharacterId && storedDailyCharacterId !== currentDailyCharacterId) {
|
if (storedDailyCharacterId && storedDailyCharacterId !== dailyCurrentCharacterId) {
|
||||||
localStorage.removeItem('dailyCharacterHistory');
|
localStorage.removeItem('dailyCharacterHistory');
|
||||||
selectedCharacters = [];
|
selectedCharacters = [];
|
||||||
} else {
|
} else {
|
||||||
@@ -47,8 +47,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Store the current daily character ID
|
// Store the current daily character ID
|
||||||
if (currentDailyCharacterId) {
|
if (dailyCurrentCharacterId) {
|
||||||
localStorage.setItem('currentDailyCharacterId', currentDailyCharacterId);
|
localStorage.setItem('dailyCurrentCharacterId', dailyCurrentCharacterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoaded = true;
|
isLoaded = true;
|
||||||
|
|||||||
28
src/routes/(game)/infinite/+page.server.ts
Normal file
28
src/routes/(game)/infinite/+page.server.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { config } from '$lib/server/db/schema';
|
||||||
|
import { getAllCharacters } from '$lib/server/daily-character';
|
||||||
|
import { like } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export async function load() {
|
||||||
|
const characters = await getAllCharacters();
|
||||||
|
|
||||||
|
// Load column visibility config
|
||||||
|
const columnConfig = await db
|
||||||
|
.select()
|
||||||
|
.from(config)
|
||||||
|
.where(like(config.key, 'characterHistory.column.%.visible'));
|
||||||
|
|
||||||
|
// Convert to object for easier access
|
||||||
|
const columnVisibility: Record<string, boolean> = {};
|
||||||
|
columnConfig.forEach(row => {
|
||||||
|
const match = row.key.match(/characterHistory\.column\.(.+)\.visible/);
|
||||||
|
if (match) {
|
||||||
|
columnVisibility[match[1]] = row.value === 'true';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
characters,
|
||||||
|
columnVisibility
|
||||||
|
};
|
||||||
|
}
|
||||||
400
src/routes/(game)/infinite/+page.svelte
Normal file
400
src/routes/(game)/infinite/+page.svelte
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { formatBounty } from '$lib';
|
||||||
|
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
||||||
|
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
let selectedCharacters: any[] = [];
|
||||||
|
let currentCharacter: any = null;
|
||||||
|
let isLoaded = false;
|
||||||
|
let score = 0;
|
||||||
|
let columnVisibility: Record<string, boolean> = {};
|
||||||
|
|
||||||
|
let wasOriginAvailable = false;
|
||||||
|
let wasFruitAvailable = false;
|
||||||
|
let wasAffiliationAvailable = false;
|
||||||
|
let showOriginUnlock = false;
|
||||||
|
let showFruitUnlock = false;
|
||||||
|
let showAffiliationUnlock = false;
|
||||||
|
|
||||||
|
// Load from localStorage on mount
|
||||||
|
onMount(() => {
|
||||||
|
const storedScore = localStorage.getItem('infiniteScore');
|
||||||
|
if (storedScore) {
|
||||||
|
score = parseInt(storedScore, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load column visibility from localStorage, fallback to server defaults
|
||||||
|
const storedColumnVisibility = localStorage.getItem('infiniteColumnVisibility');
|
||||||
|
if (storedColumnVisibility) {
|
||||||
|
try {
|
||||||
|
columnVisibility = JSON.parse(storedColumnVisibility);
|
||||||
|
} catch (e) {
|
||||||
|
columnVisibility = data.columnVisibility || {};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
columnVisibility = data.columnVisibility || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load current character ID and history IDs from localStorage
|
||||||
|
const storedCharacterId = localStorage.getItem('infiniteCurrentCharacterId');
|
||||||
|
const storedHistoryIds = localStorage.getItem('infiniteSelectedCharacterIds');
|
||||||
|
|
||||||
|
if (storedCharacterId && storedHistoryIds && characters.length > 0) {
|
||||||
|
try {
|
||||||
|
const charId = JSON.parse(storedCharacterId);
|
||||||
|
const historyIds = JSON.parse(storedHistoryIds);
|
||||||
|
|
||||||
|
// Find the character object by ID
|
||||||
|
currentCharacter = characters.find((c: any) => c.id === charId);
|
||||||
|
|
||||||
|
// Find all character objects by their IDs
|
||||||
|
selectedCharacters = historyIds
|
||||||
|
.map((id: string) => characters.find((c: any) => c.id === id))
|
||||||
|
.filter((c: any) => c !== undefined);
|
||||||
|
|
||||||
|
// If character not found, generate a new one
|
||||||
|
if (!currentCharacter) {
|
||||||
|
generateNewCharacter();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// If parsing fails, generate a new character
|
||||||
|
generateNewCharacter();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
generateNewCharacter();
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoaded = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save score to localStorage whenever it changes
|
||||||
|
$: if (isLoaded) {
|
||||||
|
localStorage.setItem('infiniteScore', score.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save column visibility to localStorage whenever it changes
|
||||||
|
$: if (isLoaded) {
|
||||||
|
localStorage.setItem('infiniteColumnVisibility', JSON.stringify(columnVisibility));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save current character ID to localStorage whenever it changes
|
||||||
|
$: if (isLoaded && currentCharacter) {
|
||||||
|
localStorage.setItem('infiniteCurrentCharacterId', JSON.stringify(currentCharacter.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save selected character IDs to localStorage whenever it changes
|
||||||
|
$: if (isLoaded) {
|
||||||
|
const selectedIds = selectedCharacters.map((c: any) => c.id);
|
||||||
|
localStorage.setItem('infiniteSelectedCharacterIds', JSON.stringify(selectedIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
$: characters = data.characters || [];
|
||||||
|
$: hasWon = currentCharacter && selectedCharacters.some(char => char.id === currentCharacter.id);
|
||||||
|
|
||||||
|
// 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)];
|
||||||
|
selectedCharacters = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCharacterSelect(event: CustomEvent) {
|
||||||
|
const character = event.detail;
|
||||||
|
selectCharacter(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCharacter(character: any) {
|
||||||
|
selectedCharacters = [character, ...selectedCharacters];
|
||||||
|
|
||||||
|
// Check if player won
|
||||||
|
if (character.id === currentCharacter.id) {
|
||||||
|
// Increment score (saved to localStorage via reactive statement)
|
||||||
|
score++;
|
||||||
|
// Don't auto-generate next character - wait for user to click "Recommencer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextCharacter() {
|
||||||
|
generateNewCharacter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetScore() {
|
||||||
|
score = 0;
|
||||||
|
selectedCharacters = [];
|
||||||
|
generateNewCharacter();
|
||||||
|
// Clear localStorage for current character and history
|
||||||
|
localStorage.removeItem('infiniteCurrentCharacterId');
|
||||||
|
localStorage.removeItem('infiniteSelectedCharacterIds');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleColumnVisibility(column: string) {
|
||||||
|
columnVisibility[column] = !columnVisibility[column];
|
||||||
|
columnVisibility = columnVisibility; // Trigger reactivity
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>OnePieceDle - Mode Infini</title>
|
||||||
|
<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);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
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);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.gecko-moria-effect {
|
||||||
|
animation: shadow-pulse 1.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main
|
||||||
|
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100"
|
||||||
|
>
|
||||||
|
<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%)]"
|
||||||
|
></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">
|
||||||
|
<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">
|
||||||
|
Mode Infini
|
||||||
|
</h1>
|
||||||
|
<p class="mt-2 text-2xl font-bold text-amber-300">Score: {score}</p>
|
||||||
|
</div>
|
||||||
|
{#if score > 0}
|
||||||
|
<button
|
||||||
|
class="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"
|
||||||
|
onclick={resetScore}
|
||||||
|
>
|
||||||
|
Réinitialiser
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<p class="max-w-2xl text-base text-slate-200 sm:text-lg">
|
||||||
|
Devine des personnages à l'infini ! Chaque indice se débloque après un certain nombre de
|
||||||
|
tentatives. Bonne chance !
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="mt-10 grid gap-6">
|
||||||
|
{#if currentCharacter}
|
||||||
|
{#if hasWon}
|
||||||
|
<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">Bien joué !</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 currentCharacter.pictureUrl}
|
||||||
|
<a
|
||||||
|
href={'https://onepiece.fandom.com/fr/wiki/' + currentCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-block"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={currentCharacter.pictureUrl}
|
||||||
|
alt={currentCharacter.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">{currentCharacter.name}</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={nextCharacter}
|
||||||
|
class="mt-4 rounded-full bg-emerald-500 px-6 py-2 text-sm font-semibold text-white transition hover:bg-emerald-600"
|
||||||
|
>
|
||||||
|
Recommencer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{: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="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={() => (showOriginUnlock = !showOriginUnlock)}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Origine</p>
|
||||||
|
{#if showOriginUnlock}
|
||||||
|
<p class="mt-2 text-xs text-white font-semibold">
|
||||||
|
{currentCharacter.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={() => (showFruitUnlock = !showFruitUnlock)}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Fruit du démon</p>
|
||||||
|
{#if showFruitUnlock}
|
||||||
|
<p class="mt-2 text-xs text-white font-semibold">
|
||||||
|
{currentCharacter.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={() => (showAffiliationUnlock = !showAffiliationUnlock)}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Affiliation</p>
|
||||||
|
{#if showAffiliationUnlock}
|
||||||
|
{@const affiliations = typeof currentCharacter.affiliations === 'string'
|
||||||
|
? currentCharacter.affiliations.includes('[')
|
||||||
|
? JSON.parse(currentCharacter.affiliations)
|
||||||
|
: currentCharacter.affiliations
|
||||||
|
.split(',')
|
||||||
|
.map((a: string) => a.trim())
|
||||||
|
: currentCharacter.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>
|
||||||
|
<CharacterSearchInput
|
||||||
|
{characters}
|
||||||
|
{selectedCharacters}
|
||||||
|
on:select={handleCharacterSelect}
|
||||||
|
/>
|
||||||
|
{/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">
|
||||||
|
<p class="text-center text-slate-300">Chargement du personnage...</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{#if currentCharacter}
|
||||||
|
<!-- Column Visibility Toggle -->
|
||||||
|
<section class="mt-10">
|
||||||
|
<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">
|
||||||
|
<h3 class="mb-4 text-lg font-semibold text-amber-300">Colonnes visibles</h3>
|
||||||
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4">
|
||||||
|
{#each Object.entries(columnVisibility) as [column, isVisible] (column)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => toggleColumnVisibility(column)}
|
||||||
|
class="rounded-lg border px-3 py-2 text-sm font-medium transition-colors {isVisible
|
||||||
|
? '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'}"
|
||||||
|
>
|
||||||
|
{column}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<GuessHistoryTable
|
||||||
|
{selectedCharacters}
|
||||||
|
dailyCharacter={currentCharacter}
|
||||||
|
{columnVisibility}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:global(.hint-unlocking) {
|
||||||
|
animation: hint-unlock 0.6s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user