feat: add WinPanel and YesterdayCharacter components; refactor daily game page
- Introduced WinPanel component to display win/loss messages based on game outcome. - Added YesterdayCharacter component to show the character from the previous day. - Refactored daily game page to utilize new components for better code organization and readability. - Updated server-side logic to fetch daily character ID and count wins more efficiently. - Cleaned up character selection logic and hint availability tracking.
This commit is contained in:
167
src/lib/components/CharacterSearchInput.svelte
Normal file
167
src/lib/components/CharacterSearchInput.svelte
Normal file
@@ -0,0 +1,167 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let characters: any[];
|
||||
export let selectedCharacters: any[];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let searchInput = '';
|
||||
let highlightedIndex = 0;
|
||||
let dropdownContainer: HTMLDivElement;
|
||||
let searchContainer: HTMLDivElement;
|
||||
|
||||
onMount(() => {
|
||||
// Add click outside listener
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
});
|
||||
|
||||
$: filteredCharacters = characters.filter(char => {
|
||||
const searchTerm = searchInput.toLowerCase();
|
||||
const nameMatches = char.name.toLowerCase().includes(searchTerm);
|
||||
|
||||
let epithetsMatches = false;
|
||||
if (char.epithets) {
|
||||
try {
|
||||
const parsedEpithets = typeof char.epithets === 'string'
|
||||
? JSON.parse(char.epithets)
|
||||
: char.epithets;
|
||||
|
||||
if (Array.isArray(parsedEpithets)) {
|
||||
epithetsMatches = parsedEpithets.some((epithet: string) =>
|
||||
epithet.toLowerCase().includes(searchTerm)
|
||||
);
|
||||
} else if (typeof parsedEpithets === 'string') {
|
||||
epithetsMatches = parsedEpithets.toLowerCase().includes(searchTerm);
|
||||
}
|
||||
} catch {
|
||||
epithetsMatches = String(char.epithets).toLowerCase().includes(searchTerm);
|
||||
}
|
||||
}
|
||||
|
||||
return (nameMatches || epithetsMatches) &&
|
||||
!selectedCharacters.some(selected => selected.id === char.id);
|
||||
});
|
||||
|
||||
// Reset highlighted index when filtered list changes
|
||||
$: if (filteredCharacters) {
|
||||
highlightedIndex = 0;
|
||||
}
|
||||
|
||||
// Scroll highlighted item into view
|
||||
$: if (dropdownContainer && highlightedIndex >= 0) {
|
||||
const highlightedButton = dropdownContainer.querySelector(
|
||||
`button:nth-child(${highlightedIndex + 1})`
|
||||
) as HTMLElement;
|
||||
if (highlightedButton) {
|
||||
highlightedButton.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
||||
}
|
||||
}
|
||||
|
||||
function selectCharacter(character: any) {
|
||||
dispatch('select', character);
|
||||
searchInput = '';
|
||||
highlightedIndex = 0;
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (filteredCharacters.length === 0) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
event.preventDefault();
|
||||
highlightedIndex = Math.min(highlightedIndex + 1, filteredCharacters.length - 1);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
event.preventDefault();
|
||||
highlightedIndex = Math.max(highlightedIndex - 1, 0);
|
||||
break;
|
||||
case 'Enter':
|
||||
event.preventDefault();
|
||||
if (filteredCharacters[highlightedIndex]) {
|
||||
selectCharacter(filteredCharacters[highlightedIndex]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function submitGuess() {
|
||||
if (filteredCharacters.length === 0) return;
|
||||
const characterToSelect =
|
||||
filteredCharacters[highlightedIndex] ?? filteredCharacters[0];
|
||||
if (characterToSelect) {
|
||||
selectCharacter(characterToSelect);
|
||||
}
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (searchContainer && !searchContainer.contains(event.target as Node)) {
|
||||
searchInput = '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<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 z-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Entrer une supposition</h2>
|
||||
<div class="mt-4 flex flex-col gap-3 sm:flex-row">
|
||||
<div bind:this={searchContainer} class="relative w-full">
|
||||
<input
|
||||
bind:value={searchInput}
|
||||
class="w-full rounded-full border border-amber-200/30 bg-slate-900/60 px-5 py-3 text-sm text-slate-100 placeholder:text-slate-400 focus:border-amber-200/70 focus:outline-none"
|
||||
placeholder="Nom du personnage"
|
||||
type="text"
|
||||
onkeydown={handleKeydown}
|
||||
/>
|
||||
{#if searchInput.length > 0 && filteredCharacters.length > 0}
|
||||
<div bind:this={dropdownContainer} class="absolute top-full left-0 right-0 mt-2 max-h-96 overflow-y-auto rounded-2xl border border-amber-200/30 bg-slate-900/90 backdrop-blur z-10">
|
||||
{#each filteredCharacters as character, index (character.id)}
|
||||
<button
|
||||
class="w-full px-5 py-4 text-left text-base text-slate-100 transition border-b border-slate-800/50 last:border-b-0 flex items-center gap-4 {index === highlightedIndex ? 'bg-slate-700' : 'hover:bg-slate-800/70'}"
|
||||
type="button"
|
||||
onmouseenter={() => highlightedIndex = index}
|
||||
onclick={() => selectCharacter(character)}
|
||||
>
|
||||
{#if character.pictureUrl}
|
||||
<img
|
||||
src={character.pictureUrl}
|
||||
alt={character.name}
|
||||
class="w-12 h-12 rounded-full object-cover border border-amber-200/30"
|
||||
/>
|
||||
{:else}
|
||||
<div class="w-12 h-12 rounded-full bg-slate-800 border border-amber-200/30 flex items-center justify-center">
|
||||
<span class="text-xs text-slate-400">?</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-1">
|
||||
<span class="font-semibold text-amber-100">{character.name}</span>
|
||||
{#if character.epithets}
|
||||
{@const parsedEpithets = typeof character.epithets === 'string'
|
||||
? JSON.parse(character.epithets)
|
||||
: character.epithets}
|
||||
{#if Array.isArray(parsedEpithets) && parsedEpithets.length > 0}
|
||||
<span class="ml-2 text-xs text-slate-400">
|
||||
• {parsedEpithets.join(', ')}
|
||||
</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onclick={submitGuess}
|
||||
disabled={searchInput.length === 0 || filteredCharacters.length === 0}
|
||||
class="rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Valider
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user