Compare commits
10 Commits
b4aa5e1a73
...
201c4759b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 201c4759b6 | |||
| 485d96026a | |||
| a339498879 | |||
| b5d53d69c1 | |||
| f31f49aec7 | |||
| 8aac371c32 | |||
| c3bc429af2 | |||
| 7157e8c5a6 | |||
| bbce1ff136 | |||
| a80e977e87 |
16
drizzle/0003_wise_blonde_phantom.sql
Normal file
16
drizzle/0003_wise_blonde_phantom.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
PRAGMA foreign_keys=OFF;--> statement-breakpoint
|
||||||
|
CREATE TABLE `__new_characterHistory` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`characterId` text,
|
||||||
|
`date` integer NOT NULL,
|
||||||
|
`won` integer DEFAULT 0 NOT NULL,
|
||||||
|
`createdAt` integer NOT NULL,
|
||||||
|
`updatedAt` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`characterId`) REFERENCES `character`(`id`) ON UPDATE no action ON DELETE no action
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
INSERT INTO `__new_characterHistory`("id", "characterId", "date", "won", "createdAt", "updatedAt") SELECT "id", "characterId", "date", "won", "createdAt", "updatedAt" FROM `characterHistory`;--> statement-breakpoint
|
||||||
|
DROP TABLE `characterHistory`;--> statement-breakpoint
|
||||||
|
ALTER TABLE `__new_characterHistory` RENAME TO `characterHistory`;--> statement-breakpoint
|
||||||
|
PRAGMA foreign_keys=ON;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `characterHistory_date_unique` ON `characterHistory` (`date`);
|
||||||
1092
drizzle/meta/0003_snapshot.json
Normal file
1092
drizzle/meta/0003_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,13 @@
|
|||||||
"when": 1772390182445,
|
"when": 1772390182445,
|
||||||
"tag": "0002_large_gwen_stacy",
|
"tag": "0002_large_gwen_stacy",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 3,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1772449624450,
|
||||||
|
"tag": "0003_wise_blonde_phantom",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -18,6 +18,7 @@
|
|||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"db:import": "npx tsx scripts/import-json.ts",
|
"db:import": "npx tsx scripts/import-json.ts",
|
||||||
"db:set-daily-mode": "npx tsx scripts/set-daily-mode.ts",
|
"db:set-daily-mode": "npx tsx scripts/set-daily-mode.ts",
|
||||||
|
"user:promote-admin": "npx tsx scripts/promote-admin.ts",
|
||||||
"auth:schema": "npx @better-auth/cli generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes",
|
"auth:schema": "npx @better-auth/cli generate --config src/lib/server/auth.ts --output src/lib/server/db/auth.schema.ts --yes",
|
||||||
"scrape": "npx tsx scripts/scrape-onepiece.ts"
|
"scrape": "npx tsx scripts/scrape-onepiece.ts"
|
||||||
},
|
},
|
||||||
|
|||||||
55
scripts/promote-admin.ts
Normal file
55
scripts/promote-admin.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { createClient } from '@libsql/client';
|
||||||
|
import { drizzle } from 'drizzle-orm/libsql';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
import { user } from '../src/lib/server/db/auth.schema';
|
||||||
|
|
||||||
|
const DATABASE_URL = process.env.DATABASE_URL || 'file:local.db';
|
||||||
|
|
||||||
|
function getErrorMessage(error: unknown): string {
|
||||||
|
return error instanceof Error ? error.message : String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function promoteAdmin(): Promise<void> {
|
||||||
|
const email = process.argv[2]?.trim();
|
||||||
|
|
||||||
|
if (!email) {
|
||||||
|
console.error('❌ Missing email argument');
|
||||||
|
console.error('Usage: npm run user:promote-admin -- <email>');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const client = createClient({ url: DATABASE_URL });
|
||||||
|
const db = drizzle(client);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const existingUsers = await db.select().from(user).where(eq(user.email, email)).limit(1);
|
||||||
|
const targetUser = existingUsers[0];
|
||||||
|
|
||||||
|
if (!targetUser) {
|
||||||
|
console.error(`❌ User not found for email: ${email}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetUser.isAdmin) {
|
||||||
|
console.log(`ℹ️ User is already admin: ${targetUser.email}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(user)
|
||||||
|
.set({ isAdmin: true })
|
||||||
|
.where(eq(user.id, targetUser.id));
|
||||||
|
|
||||||
|
console.log(`✅ Admin granted to: ${targetUser.email}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to promote admin: ${getErrorMessage(error)}`);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
promoteAdmin().catch((error) => {
|
||||||
|
console.error(getErrorMessage(error));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
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>
|
||||||
246
src/lib/components/GuessHistoryTable.svelte
Normal file
246
src/lib/components/GuessHistoryTable.svelte
Normal file
@@ -0,0 +1,246 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { formatBounty } from '$lib';
|
||||||
|
|
||||||
|
export let selectedCharacters: any[];
|
||||||
|
export let dailyCharacter: any;
|
||||||
|
export let columnVisibility: any;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="mt-8 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="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-col items-center gap-4 text-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p>
|
||||||
|
</div>
|
||||||
|
{#if selectedCharacters.length === 0}
|
||||||
|
<p class="text-sm text-slate-200 text-center">Aucune tentative pour le moment.</p>
|
||||||
|
{:else}
|
||||||
|
<div class="overflow-x-auto pb-2 -mx-6 px-6 sm:mx-0 sm:px-0">
|
||||||
|
<div class="w-max min-w-max mx-auto">
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Personnage</p>
|
||||||
|
</div>
|
||||||
|
{#if columnVisibility.status !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Statut</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.gender !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Genre</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.affiliations !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Affiliations</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.devilFruitType !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Fruit</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.haki !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Haki</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.bounty !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Prime</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.height !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Taille</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.origin !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Origine</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if columnVisibility.arc !== false}
|
||||||
|
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Arc</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rows -->
|
||||||
|
{#each selectedCharacters as character (character.id)}
|
||||||
|
<div class="flex gap-2 mb-2">
|
||||||
|
<!-- Personnage -->
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 bg-slate-950/60 overflow-hidden">
|
||||||
|
{#if character.pictureUrl}
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + character.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="block w-full h-full"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={character.pictureUrl}
|
||||||
|
alt={character.name}
|
||||||
|
class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div class="w-full h-full bg-slate-800 flex items-center justify-center p-2">
|
||||||
|
<span class="text-xl text-center font-semibold line-clamp-3">{character.name}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Vivant / Mort -->
|
||||||
|
{#if columnVisibility.status !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.status === dailyCharacter.status ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-sm font-bold text-white text-center">
|
||||||
|
{character.status === 'Alive' ? 'Vivant' : character.status === 'Deceased' || character.status === 'Dead' ? 'Mort' : character.status || 'Inconnu'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Genre -->
|
||||||
|
{#if columnVisibility.gender !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.gender === dailyCharacter.gender ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-base font-bold text-white text-center">
|
||||||
|
{character.gender === 'Male' ? 'Homme' : character.gender === 'Female' ? 'Femme' : character.gender || 'Inconnu'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Affiliations -->
|
||||||
|
{#if columnVisibility.affiliations !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
|
||||||
|
try {
|
||||||
|
const charAff = typeof character.affiliations === 'string'
|
||||||
|
? ((character.affiliations as string).includes('[') ? JSON.parse(character.affiliations) : (character.affiliations as string).split(',').map((a: string) => a.trim()))
|
||||||
|
: character.affiliations;
|
||||||
|
const dailyAff = typeof dailyCharacter.affiliations === 'string'
|
||||||
|
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
|
||||||
|
: dailyCharacter.affiliations;
|
||||||
|
const charFirstAff = Array.isArray(charAff) ? charAff[0] : charAff;
|
||||||
|
const dailyFirstAff = Array.isArray(dailyAff) ? dailyAff[0] : dailyAff;
|
||||||
|
return charFirstAff && dailyFirstAff && charFirstAff === dailyFirstAff ? 'bg-emerald-600/90' : 'bg-red-900/60';
|
||||||
|
} catch (e) {
|
||||||
|
return 'bg-slate-950/60';
|
||||||
|
}
|
||||||
|
})()} p-2 flex items-center justify-center overflow-hidden">
|
||||||
|
{#if character.affiliations}
|
||||||
|
{@const parsedAffiliations = typeof character.affiliations === 'string'
|
||||||
|
? (character.affiliations.includes('[') ? JSON.parse(character.affiliations) : character.affiliations.split(',').map((a: string) => a.trim()))
|
||||||
|
: character.affiliations}
|
||||||
|
{#if Array.isArray(parsedAffiliations) && parsedAffiliations.length > 0}
|
||||||
|
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations[0]}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations}</p>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p class="text-base font-bold text-slate-400 text-center">-</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Fruit -->
|
||||||
|
{#if columnVisibility.devilFruitType !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.devilFruitType === dailyCharacter.devilFruitType ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
{#if character.devilFruitType}
|
||||||
|
<p class="text-sm font-bold text-white text-center">{character.devilFruitType}</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-5xl font-bold text-white text-center">✕</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Haki -->
|
||||||
|
{#if columnVisibility.haki !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
|
||||||
|
if (character.hakiObservation === dailyCharacter.hakiObservation && character.hakiArmament === dailyCharacter.hakiArmament && character.hakiConqueror === dailyCharacter.hakiConqueror) {
|
||||||
|
return 'bg-emerald-600/90';
|
||||||
|
} else if ((character.hakiObservation && dailyCharacter.hakiObservation) ||
|
||||||
|
(character.hakiArmament && dailyCharacter.hakiArmament) ||
|
||||||
|
(character.hakiConqueror && dailyCharacter.hakiConqueror)) {
|
||||||
|
return 'bg-yellow-600/80';
|
||||||
|
} else {
|
||||||
|
return 'bg-red-900/60';
|
||||||
|
}
|
||||||
|
})()} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-2xl font-bold text-white text-center">
|
||||||
|
{#if character.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
|
||||||
|
{#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
|
||||||
|
{#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
|
||||||
|
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
|
||||||
|
<span class="text-5xl">✕</span>
|
||||||
|
{/if}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Prime -->
|
||||||
|
{#if columnVisibility.bounty !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.bounty === dailyCharacter.bounty ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
||||||
|
{#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
|
||||||
|
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||||
|
background-color: rgb(203, 213, 225);
|
||||||
|
clip-path: {character.bounty > dailyCharacter.bounty
|
||||||
|
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
||||||
|
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
||||||
|
"></div>
|
||||||
|
{/if}
|
||||||
|
{#if character.bounty != null}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">{formatBounty(character.bounty)} ฿</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Taille -->
|
||||||
|
{#if columnVisibility.height !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.height === dailyCharacter.height ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
||||||
|
{#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
|
||||||
|
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||||
|
background-color: rgb(203, 213, 225);
|
||||||
|
clip-path: {character.height > dailyCharacter.height
|
||||||
|
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
||||||
|
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
||||||
|
"></div>
|
||||||
|
{/if}
|
||||||
|
{#if character.height}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">{character.height} m</p>
|
||||||
|
{:else}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Origine -->
|
||||||
|
{#if columnVisibility.origin !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.origin === dailyCharacter.origin ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
||||||
|
<p class="text-sm font-bold text-white text-center">{character.origin || 'Inconnue'}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Arc -->
|
||||||
|
{#if columnVisibility.arc !== false}
|
||||||
|
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.arcName === dailyCharacter.arcName ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
||||||
|
{#if character.arcName !== dailyCharacter.arcName && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
|
||||||
|
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
||||||
|
background-color: rgb(203, 213, 225);
|
||||||
|
clip-path: {character.firstAppearance > dailyCharacter.firstAppearance
|
||||||
|
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
||||||
|
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
||||||
|
"></div>
|
||||||
|
{/if}
|
||||||
|
<p class="text-sm font-bold text-white text-center relative z-10">{character.arcName || 'Inconnu'}</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
88
src/lib/components/HintsPanel.svelte
Normal file
88
src/lib/components/HintsPanel.svelte
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let dailyCharacter: any;
|
||||||
|
export let selectedCharacters: any[];
|
||||||
|
export let showOriginUnlock: boolean = false;
|
||||||
|
export let showFruitUnlock: boolean = false;
|
||||||
|
export let showAffiliationUnlock: boolean = false;
|
||||||
|
|
||||||
|
let showHintOrigin = false;
|
||||||
|
let showHintFruit = false;
|
||||||
|
let showHintAffiliation = false;
|
||||||
|
|
||||||
|
// Hint availability - indices are available after a certain number of guesses
|
||||||
|
$: isOriginAvailable = selectedCharacters.length >= 5;
|
||||||
|
$: isFruitAvailable = selectedCharacters.length >= 10;
|
||||||
|
$: isAffiliationAvailable = selectedCharacters.length >= 15;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.hint-unlocking {
|
||||||
|
animation: hint-unlock 0.6s ease-out;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<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={() => showHintOrigin = !showHintOrigin}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Origine</p>
|
||||||
|
{#if showHintOrigin}
|
||||||
|
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.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={() => showHintFruit = !showHintFruit}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Fruit du démon</p>
|
||||||
|
{#if showHintFruit}
|
||||||
|
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.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={() => showHintAffiliation = !showHintAffiliation}
|
||||||
|
>
|
||||||
|
<p class="text-sm font-medium text-amber-100">Affiliation</p>
|
||||||
|
{#if showHintAffiliation}
|
||||||
|
{@const affiliations = typeof dailyCharacter.affiliations === 'string'
|
||||||
|
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
|
||||||
|
: dailyCharacter.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>
|
||||||
57
src/lib/components/WinPanel.svelte
Normal file
57
src/lib/components/WinPanel.svelte
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let dailyCharacter: any;
|
||||||
|
export let selectedCharacters: any[];
|
||||||
|
export let isGeckoMoriaWin: boolean = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isGeckoMoriaWin}
|
||||||
|
<div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-3xl mb-2">🌑</div>
|
||||||
|
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
|
||||||
|
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
|
||||||
|
<div class="mt-3">
|
||||||
|
{#if dailyCharacter.pictureUrl}
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-block"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={dailyCharacter.pictureUrl}
|
||||||
|
alt={dailyCharacter.name}
|
||||||
|
class="w-20 h-20 mx-auto rounded-full border-2 border-slate-600 shadow-lg object-cover hover:border-slate-500 transition-colors cursor-pointer opacity-80"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
{/if}
|
||||||
|
<p class="mt-2 text-lg font-bold text-slate-200">{dailyCharacter.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<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">Félicitations !</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 dailyCharacter.pictureUrl}
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-block"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={dailyCharacter.pictureUrl}
|
||||||
|
alt={dailyCharacter.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">{dailyCharacter.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
51
src/lib/components/YesterdayCharacter.svelte
Normal file
51
src/lib/components/YesterdayCharacter.svelte
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
export let yesterdayCharacter: any;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
||||||
|
{#if yesterdayCharacter}
|
||||||
|
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
||||||
|
{#if yesterdayCharacter.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={yesterdayCharacter.pictureUrl}
|
||||||
|
alt={yesterdayCharacter.name}
|
||||||
|
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
|
||||||
|
/>
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
||||||
|
Photo
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
||||||
|
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
|
||||||
|
{#if yesterdayCharacter.epithets}
|
||||||
|
<p class="mt-1 text-sm text-slate-400">
|
||||||
|
{typeof yesterdayCharacter.epithets === 'string'
|
||||||
|
? JSON.parse(yesterdayCharacter.epithets).join(', ')
|
||||||
|
: (yesterdayCharacter.epithets as string[]).join(', ')}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="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 sm:w-auto"
|
||||||
|
>
|
||||||
|
Voir la page
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
||||||
|
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
||||||
|
Photo
|
||||||
|
</div>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
||||||
|
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
|
||||||
|
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</section>
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { arc, character, characterHistory, characterOverride, devilFruit } from '$lib/server/db/schema';
|
import { arc, character, characterHistory, characterOverride, devilFruit } from '$lib/server/db/schema';
|
||||||
import { desc, eq, inArray } from 'drizzle-orm';
|
import { desc, eq, inArray, and } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// Generate or get random seed for daily character selection
|
||||||
|
const RANDOM_SEED = Math.random();
|
||||||
|
|
||||||
const characterWithRelationsSelect = {
|
const characterWithRelationsSelect = {
|
||||||
id: character.id,
|
id: character.id,
|
||||||
@@ -139,21 +142,22 @@ async function applyCharacterOverrides(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDateKey(date: Date): string {
|
export function getDateKey(date: Date): number {
|
||||||
return date.toISOString().split('T')[0];
|
return normalizeDay(date).getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeDay(date: Date = new Date()): Date {
|
export function normalizeDay(date: Date = new Date()): Date {
|
||||||
const normalized = new Date(date);
|
const normalized = new Date(date);
|
||||||
normalized.setHours(1, 0, 0, 0);
|
normalized.setHours(1, 0, 0, 0);
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickDailyCharacter(characters: CharacterWithRelations[], date: Date): CharacterWithRelations {
|
function pickDailyCharacter(characters: CharacterWithRelations[], date: Date): CharacterWithRelations {
|
||||||
const dateStr = getDateKey(date);
|
const timestamp = getDateKey(date);
|
||||||
const seed = dateStr.split('-').reduce((acc, value) => acc + parseInt(value), 0);
|
const daysSinceEpoch = Math.floor(timestamp / 1000 / 60 / 60 / 24);
|
||||||
const index = seed % characters.length;
|
// Combine timestamp with random seed to avoid predictable results
|
||||||
return characters[index];
|
const combinedSeed = (daysSinceEpoch + Math.floor(RANDOM_SEED * 1000000)) % characters.length;
|
||||||
|
return characters[combinedSeed];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> {
|
export async function getDailyModeCharacters(): Promise<CharacterWithRelations[]> {
|
||||||
@@ -168,6 +172,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)
|
||||||
@@ -186,10 +201,12 @@ export async function getCharacterById(characterId: string): Promise<CharacterWi
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getOrCreateTodayCharacter(
|
export async function getOrCreateTodayCharacter(
|
||||||
characters: CharacterWithRelations[],
|
characters?: CharacterWithRelations[],
|
||||||
date: Date = new Date()
|
date: Date = new Date()
|
||||||
): Promise<CharacterWithRelations | null> {
|
): Promise<CharacterWithRelations | null> {
|
||||||
if (characters.length === 0) {
|
const dailyCharacters = characters ?? (await getDailyModeCharacters());
|
||||||
|
|
||||||
|
if (dailyCharacters.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,7 +221,7 @@ export async function getOrCreateTodayCharacter(
|
|||||||
|
|
||||||
if (existingEntry?.characterId) {
|
if (existingEntry?.characterId) {
|
||||||
return (
|
return (
|
||||||
characters.find((currentCharacter) => currentCharacter.id === existingEntry.characterId) ??
|
dailyCharacters.find((currentCharacter) => currentCharacter.id === existingEntry.characterId) ??
|
||||||
(await getCharacterById(existingEntry.characterId))
|
(await getCharacterById(existingEntry.characterId))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -216,10 +233,10 @@ export async function getOrCreateTodayCharacter(
|
|||||||
.limit(100);
|
.limit(100);
|
||||||
|
|
||||||
const excludedIds = new Set(recentHistory.map((entry) => entry.characterId));
|
const excludedIds = new Set(recentHistory.map((entry) => entry.characterId));
|
||||||
const availableCharacters = characters.filter((currentCharacter) => !excludedIds.has(currentCharacter.id));
|
const availableCharacters = dailyCharacters.filter((currentCharacter) => !excludedIds.has(currentCharacter.id));
|
||||||
|
|
||||||
const dailyCharacter = pickDailyCharacter(
|
const dailyCharacter = pickDailyCharacter(
|
||||||
availableCharacters.length > 0 ? availableCharacters : characters,
|
availableCharacters.length > 0 ? availableCharacters : dailyCharacters,
|
||||||
today
|
today
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -241,9 +258,9 @@ export async function getYesterdayCharacter(
|
|||||||
date: Date = new Date(),
|
date: Date = new Date(),
|
||||||
characters?: CharacterWithRelations[]
|
characters?: CharacterWithRelations[]
|
||||||
): Promise<CharacterWithRelations | null> {
|
): Promise<CharacterWithRelations | null> {
|
||||||
const baseDate = normalizeDay(date);
|
const yesterday = new Date(date);
|
||||||
baseDate.setDate(baseDate.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
const yesterdayDate = getDateKey(baseDate);
|
const yesterdayDate = getDateKey(yesterday);
|
||||||
|
|
||||||
const [yesterdayEntry] = await db
|
const [yesterdayEntry] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -264,3 +281,23 @@ export async function getYesterdayCharacter(
|
|||||||
|
|
||||||
return getCharacterById(yesterdayEntry.characterId);
|
return getCharacterById(yesterdayEntry.characterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getTodayCharacterWinsCount(
|
||||||
|
characterId: string,
|
||||||
|
date: Date = new Date()
|
||||||
|
): Promise<number> {
|
||||||
|
const today = normalizeDay(date);
|
||||||
|
const todayDate = getDateKey(today);
|
||||||
|
|
||||||
|
const [result] = await db
|
||||||
|
.select({ won: characterHistory.won })
|
||||||
|
.from(characterHistory)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(characterHistory.characterId, characterId),
|
||||||
|
eq(characterHistory.date, todayDate)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return result?.won ?? 0;
|
||||||
|
}
|
||||||
@@ -100,7 +100,7 @@ export const characterHistory = sqliteTable('characterHistory', {
|
|||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
characterId: text('characterId').references(() => character.id),
|
characterId: text('characterId').references(() => character.id),
|
||||||
date: text('date'),
|
date: integer('date').notNull().unique(),
|
||||||
won: integer('won').notNull().default(0),
|
won: integer('won').notNull().default(0),
|
||||||
createdAt: integer('createdAt').notNull().$default(() => Date.now()),
|
createdAt: integer('createdAt').notNull().$default(() => Date.now()),
|
||||||
updatedAt: integer('updatedAt').notNull().$default(() => Date.now()),
|
updatedAt: integer('updatedAt').notNull().$default(() => Date.now()),
|
||||||
|
|||||||
@@ -1,27 +1,23 @@
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { character, devilFruit, arc, user, config, characterHistory } from '$lib/server/db/schema';
|
import { character, devilFruit, arc, user } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { getOrCreateTodayCharacter, getTodayCharacterWinsCount } from '$lib/server/daily-character';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
const [characters, devilFruits, arcs, users, configEntries, history] = await Promise.all([
|
const [characters, devilFruits, arcs, users] = await Promise.all([
|
||||||
db.select().from(character),
|
db.select().from(character),
|
||||||
db.select().from(devilFruit),
|
db.select().from(devilFruit),
|
||||||
db.select().from(arc),
|
db.select().from(arc),
|
||||||
db.select().from(user),
|
db.select().from(user)
|
||||||
db.select().from(config),
|
|
||||||
db.select().from(characterHistory)
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get daily character ID from config
|
// Get today's daily character and count wins
|
||||||
const dailyCharIdEntry = configEntries.find((c) => c.key === 'dailyCharacterId');
|
const todayCharacter = await getOrCreateTodayCharacter();
|
||||||
const dailyCharId = dailyCharIdEntry?.value;
|
|
||||||
|
|
||||||
// Count how many times today's daily character was won/found
|
let dailyCharacterWins = 0;
|
||||||
const today = new Date().toISOString().split('T')[0];
|
if (todayCharacter) {
|
||||||
const dailyCharacterWins = dailyCharId
|
dailyCharacterWins = await getTodayCharacterWinsCount(todayCharacter.id);
|
||||||
? history.filter((h) => h.characterId === dailyCharId && h.date === today && h.won === 1).length
|
}
|
||||||
: 0;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stats: {
|
stats: {
|
||||||
|
|||||||
@@ -85,24 +85,33 @@ export const actions: Actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updates: Record<string, any> = {
|
const [originalCharacter] = await db
|
||||||
// Initialize boolean fields to false (they'll be set to true if present in formData)
|
.select({
|
||||||
hakiObservation: false,
|
hakiObservation: character.hakiObservation,
|
||||||
hakiArmament: false,
|
hakiArmament: character.hakiArmament,
|
||||||
hakiConqueror: false
|
hakiConqueror: character.hakiConqueror
|
||||||
};
|
})
|
||||||
|
.from(character)
|
||||||
|
.where(eq(character.id, id))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!originalCharacter) {
|
||||||
|
return fail(404, { error: 'Character not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updates: Record<string, any> = {};
|
||||||
|
|
||||||
formData.forEach((value, key) => {
|
formData.forEach((value, key) => {
|
||||||
if (key !== 'id') {
|
if (key !== 'id') {
|
||||||
// Handle checkboxes (haki fields)
|
|
||||||
if (key === 'hakiObservation' || key === 'hakiArmament' || key === 'hakiConqueror') {
|
|
||||||
updates[key] = value === 'on';
|
|
||||||
}
|
|
||||||
// Handle integers (age, bounty, height, devilFruitId, arcId)
|
// Handle integers (age, bounty, height, devilFruitId, arcId)
|
||||||
else if (key === 'age' || key === 'bounty' || key === 'height' || key === 'devilFruitId' || key === 'arcId') {
|
if (key === 'age' || key === 'bounty' || key === 'height' || key === 'devilFruitId' || key === 'arcId') {
|
||||||
const strValue = value as string;
|
const strValue = value as string;
|
||||||
updates[key] = strValue && strValue !== '' ? parseInt(strValue) : null;
|
updates[key] = strValue && strValue !== '' ? parseInt(strValue) : null;
|
||||||
}
|
}
|
||||||
|
// Handle checkboxes (haki fields) after parsing all form data
|
||||||
|
else if (key === 'hakiObservation' || key === 'hakiArmament' || key === 'hakiConqueror') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Handle strings (name, gender, status, origin, affiliations, epithets, pictureUrl, url, firstAppearance)
|
// Handle strings (name, gender, status, origin, affiliations, epithets, pictureUrl, url, firstAppearance)
|
||||||
else {
|
else {
|
||||||
updates[key] = value || null;
|
updates[key] = value || null;
|
||||||
@@ -110,6 +119,17 @@ export const actions: Actions = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const submittedHakiObservation = formData.has('hakiObservation');
|
||||||
|
const submittedHakiArmament = formData.has('hakiArmament');
|
||||||
|
const submittedHakiConqueror = formData.has('hakiConqueror');
|
||||||
|
|
||||||
|
updates.hakiObservation =
|
||||||
|
submittedHakiObservation === originalCharacter.hakiObservation ? null : submittedHakiObservation;
|
||||||
|
updates.hakiArmament =
|
||||||
|
submittedHakiArmament === originalCharacter.hakiArmament ? null : submittedHakiArmament;
|
||||||
|
updates.hakiConqueror =
|
||||||
|
submittedHakiConqueror === originalCharacter.hakiConqueror ? null : submittedHakiConqueror;
|
||||||
|
|
||||||
// Update or insert into characterOverride table
|
// Update or insert into characterOverride table
|
||||||
await db
|
await db
|
||||||
.insert(characterOverride)
|
.insert(characterOverride)
|
||||||
|
|||||||
@@ -148,9 +148,9 @@
|
|||||||
pictureUrl: override.pictureUrl ?? '',
|
pictureUrl: override.pictureUrl ?? '',
|
||||||
url: override.url ?? '',
|
url: override.url ?? '',
|
||||||
devilFruitId: override.devilFruitId !== null && override.devilFruitId !== undefined ? override.devilFruitId : '',
|
devilFruitId: override.devilFruitId !== null && override.devilFruitId !== undefined ? override.devilFruitId : '',
|
||||||
hakiObservation: override.hakiObservation ?? false,
|
hakiObservation: override.hakiObservation ?? char.hakiObservation,
|
||||||
hakiArmament: override.hakiArmament ?? false,
|
hakiArmament: override.hakiArmament ?? char.hakiArmament,
|
||||||
hakiConqueror: override.hakiConqueror ?? false,
|
hakiConqueror: override.hakiConqueror ?? char.hakiConqueror,
|
||||||
firstAppearance: override.firstAppearance ?? '',
|
firstAppearance: override.firstAppearance ?? '',
|
||||||
arcId: override.arcId !== null && override.arcId !== undefined ? override.arcId : '',
|
arcId: override.arcId !== null && override.arcId !== undefined ? override.arcId : '',
|
||||||
status: override.status ?? ''
|
status: override.status ?? ''
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100"
|
class="relative min-h-[calc(100vh-5rem)] 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 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="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 items-center justify-center px-6 py-10">
|
<div class="relative mx-auto flex w-full max-w-6xl flex-col items-center justify-center px-6 py-10">
|
||||||
<div class="flex w-full flex-col items-center gap-8">
|
<div class="flex w-full flex-col items-center gap-8">
|
||||||
<div class="text-center mb-12">
|
<div class="text-center mb-12">
|
||||||
<h1 class="text-4xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-6xl">
|
<h1 class="text-4xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-6xl">
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { formatBounty } from '$lib';
|
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
|
||||||
|
import HintsPanel from '$lib/components/HintsPanel.svelte';
|
||||||
|
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
||||||
|
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
|
||||||
|
import WinPanel from '$lib/components/WinPanel.svelte';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
|
|
||||||
let searchInput = '';
|
|
||||||
let selectedCharacters: any[] = [];
|
let selectedCharacters: any[] = [];
|
||||||
let highlightedIndex = 0;
|
|
||||||
let isLoaded = false;
|
let isLoaded = false;
|
||||||
let isGeckoMoriaWin = false;
|
let isGeckoMoriaWin = false;
|
||||||
let dropdownContainer: HTMLDivElement;
|
|
||||||
let showHintOrigin = false;
|
|
||||||
let showHintFruit = false;
|
|
||||||
let showHintAffiliation = false;
|
|
||||||
|
|
||||||
let wasOriginAvailable = false;
|
let wasOriginAvailable = false;
|
||||||
let wasFruitAvailable = false;
|
let wasFruitAvailable = false;
|
||||||
@@ -23,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 {
|
||||||
@@ -49,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;
|
||||||
@@ -68,10 +66,10 @@
|
|||||||
$: columnVisibility = data.columnVisibility || {};
|
$: columnVisibility = data.columnVisibility || {};
|
||||||
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id);
|
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id);
|
||||||
|
|
||||||
// Hint availability - indices are available after a certain number of guesses
|
// Hint availability tracking for unlock animations
|
||||||
$: isOriginAvailable = selectedCharacters.length >= 5; // Always available
|
$: isOriginAvailable = selectedCharacters.length >= 5;
|
||||||
$: isFruitAvailable = selectedCharacters.length >= 10; // Available after 5 guesses
|
$: isFruitAvailable = selectedCharacters.length >= 10;
|
||||||
$: isAffiliationAvailable = selectedCharacters.length >= 15; // Available after 10 guesses
|
$: isAffiliationAvailable = selectedCharacters.length >= 15;
|
||||||
|
|
||||||
// Track hint unlocks
|
// Track hint unlocks
|
||||||
$: if (isLoaded) {
|
$: if (isLoaded) {
|
||||||
@@ -94,52 +92,13 @@
|
|||||||
wasAffiliationAvailable = isAffiliationAvailable;
|
wasAffiliationAvailable = isAffiliationAvailable;
|
||||||
}
|
}
|
||||||
|
|
||||||
$: filteredCharacters = characters.filter(char => {
|
function handleCharacterSelect(event: CustomEvent) {
|
||||||
const searchTerm = searchInput.toLowerCase();
|
const character = event.detail;
|
||||||
const nameMatches = char.name.toLowerCase().includes(searchTerm);
|
selectCharacter(character);
|
||||||
|
|
||||||
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) {
|
function selectCharacter(character: any) {
|
||||||
selectedCharacters = [character, ...selectedCharacters];
|
selectedCharacters = [character, ...selectedCharacters];
|
||||||
searchInput = '';
|
|
||||||
highlightedIndex = 0;
|
|
||||||
|
|
||||||
// Check if player won
|
// Check if player won
|
||||||
if (character.id === dailyCharacter.id) {
|
if (character.id === dailyCharacter.id) {
|
||||||
@@ -165,55 +124,11 @@
|
|||||||
selectedCharacters = [];
|
selectedCharacters = [];
|
||||||
localStorage.removeItem('dailyCharacterHistory');
|
localStorage.removeItem('dailyCharacterHistory');
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>OnePieceDle - Mode du jour</title>
|
<title>OnePieceDle - Mode du jour</title>
|
||||||
<style>
|
<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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.hint-unlocking {
|
|
||||||
animation: hint-unlock 0.6s ease-out;
|
|
||||||
}
|
|
||||||
@keyframes shadow-pulse {
|
@keyframes shadow-pulse {
|
||||||
0% {
|
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);
|
||||||
@@ -307,460 +222,36 @@
|
|||||||
|
|
||||||
<section class="mt-10 grid gap-6">
|
<section class="mt-10 grid gap-6">
|
||||||
{#if selectedCharacters.length > 0 && !hasWon}
|
{#if selectedCharacters.length > 0 && !hasWon}
|
||||||
<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">
|
<HintsPanel
|
||||||
<div class="grid gap-3 sm:grid-cols-3">
|
{dailyCharacter}
|
||||||
<button
|
{selectedCharacters}
|
||||||
type="button"
|
{showOriginUnlock}
|
||||||
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' : ''}"
|
{showFruitUnlock}
|
||||||
disabled={!isOriginAvailable}
|
{showAffiliationUnlock}
|
||||||
onclick={() => showHintOrigin = !showHintOrigin}
|
/>
|
||||||
>
|
|
||||||
<p class="text-sm font-medium text-amber-100">Origine</p>
|
|
||||||
{#if showHintOrigin}
|
|
||||||
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.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={() => showHintFruit = !showHintFruit}
|
|
||||||
>
|
|
||||||
<p class="text-sm font-medium text-amber-100">Fruit du démon</p>
|
|
||||||
{#if showHintFruit}
|
|
||||||
<p class="mt-2 text-xs text-white font-semibold">{dailyCharacter.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={() => showHintAffiliation = !showHintAffiliation}
|
|
||||||
>
|
|
||||||
<p class="text-sm font-medium text-amber-100">Affiliation</p>
|
|
||||||
{#if showHintAffiliation}
|
|
||||||
{@const affiliations = typeof dailyCharacter.affiliations === 'string'
|
|
||||||
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
|
|
||||||
: dailyCharacter.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>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if hasWon}
|
{#if hasWon}
|
||||||
{#if isGeckoMoriaWin}
|
<WinPanel
|
||||||
<div class="rounded-3xl border border-slate-700/80 bg-slate-950/80 p-4 shadow-[0_24px_60px_rgba(0,0,0,0.8)] backdrop-blur gecko-moria-effect">
|
{dailyCharacter}
|
||||||
<div class="text-center">
|
{selectedCharacters}
|
||||||
<div class="text-3xl mb-2">🌑</div>
|
{isGeckoMoriaWin}
|
||||||
<h2 class="text-xl font-bold text-slate-300 mb-1">Moria vous contrôle...</h2>
|
|
||||||
<p class="text-sm text-slate-400">Vous avez succombé à l'ombre en {selectedCharacters.length} {selectedCharacters.length > 1 ? 'tentatives' : 'tentative'} !</p>
|
|
||||||
<div class="mt-3">
|
|
||||||
{#if dailyCharacter.pictureUrl}
|
|
||||||
<a
|
|
||||||
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="inline-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={dailyCharacter.pictureUrl}
|
|
||||||
alt={dailyCharacter.name}
|
|
||||||
class="w-20 h-20 mx-auto rounded-full border-2 border-slate-600 shadow-lg object-cover hover:border-slate-500 transition-colors cursor-pointer opacity-80"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
<p class="mt-2 text-lg font-bold text-slate-200">{dailyCharacter.name}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<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">Félicitations !</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 dailyCharacter.pictureUrl}
|
|
||||||
<a
|
|
||||||
href={"https://onepiece.fandom.com/fr/wiki/" + dailyCharacter.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="inline-block"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={dailyCharacter.pictureUrl}
|
|
||||||
alt={dailyCharacter.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">{dailyCharacter.name}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/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 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 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}
|
{:else}
|
||||||
<div class="w-12 h-12 rounded-full bg-slate-800 border border-amber-200/30 flex items-center justify-center">
|
<CharacterSearchInput
|
||||||
<span class="text-xs text-slate-400">?</span>
|
{characters}
|
||||||
</div>
|
{selectedCharacters}
|
||||||
{/if}
|
on:select={handleCharacterSelect}
|
||||||
<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={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>
|
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
<GuessHistoryTable
|
||||||
<div class="flex flex-col gap-4">
|
{selectedCharacters}
|
||||||
<div class="flex flex-col items-center gap-4 text-center">
|
{dailyCharacter}
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Historique</p>
|
{columnVisibility}
|
||||||
</div>
|
|
||||||
{#if selectedCharacters.length === 0}
|
|
||||||
<p class="text-sm text-slate-200 text-center">Aucune tentative pour le moment.</p>
|
|
||||||
{:else}
|
|
||||||
<div class="overflow-x-auto pb-2 -mx-6 px-6 sm:mx-0 sm:px-0">
|
|
||||||
<div class="w-max min-w-max mx-auto">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex gap-2 mb-2">
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Personnage</p>
|
|
||||||
</div>
|
|
||||||
{#if columnVisibility.status !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Statut</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.gender !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Genre</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.affiliations !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Affiliations</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.devilFruitType !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Fruit</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.haki !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Haki</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.bounty !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Prime</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.height !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Taille</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.origin !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Origine</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if columnVisibility.arc !== false}
|
|
||||||
<div class="w-24 shrink-0 rounded-lg border border-amber-200/30 bg-amber-900/30 p-2 text-center flex items-center justify-center">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-wider text-amber-100">Arc</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rows -->
|
|
||||||
{#each selectedCharacters as character (character.id)}
|
|
||||||
<div class="flex gap-2 mb-2">
|
|
||||||
<!-- Personnage -->
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 bg-slate-950/60 overflow-hidden">
|
|
||||||
{#if character.pictureUrl}
|
|
||||||
<a
|
|
||||||
href={"https://onepiece.fandom.com/fr/wiki/" + character.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="block w-full h-full"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={character.pictureUrl}
|
|
||||||
alt={character.name}
|
|
||||||
class="w-full h-full object-cover hover:opacity-80 transition-opacity cursor-pointer"
|
|
||||||
/>
|
/>
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<div class="w-full h-full bg-slate-800 flex items-center justify-center p-2">
|
|
||||||
<span class="text-xl text-center font-semibold line-clamp-3">{character.name}</span>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Vivant / Mort -->
|
<YesterdayCharacter {yesterdayCharacter} />
|
||||||
{#if columnVisibility.status !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.status === dailyCharacter.status ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
|
||||||
<p class="text-sm font-bold text-white text-center">
|
|
||||||
{character.status === 'Alive' ? 'Vivant' : character.status === 'Deceased' || character.status === 'Dead' ? 'Mort' : character.status || 'Inconnu'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Genre -->
|
|
||||||
{#if columnVisibility.gender !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.gender === dailyCharacter.gender ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
|
||||||
<p class="text-base font-bold text-white text-center">
|
|
||||||
{character.gender === 'Male' ? 'Homme' : character.gender === 'Female' ? 'Femme' : character.gender || 'Inconnu'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Affiliations -->
|
|
||||||
{#if columnVisibility.affiliations !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
|
|
||||||
try {
|
|
||||||
const charAff = typeof character.affiliations === 'string'
|
|
||||||
? ((character.affiliations as string).includes('[') ? JSON.parse(character.affiliations) : (character.affiliations as string).split(',').map((a: string) => a.trim()))
|
|
||||||
: character.affiliations;
|
|
||||||
const dailyAff = typeof dailyCharacter.affiliations === 'string'
|
|
||||||
? ((dailyCharacter.affiliations as string).includes('[') ? JSON.parse(dailyCharacter.affiliations) : (dailyCharacter.affiliations as string).split(',').map((a: string) => a.trim()))
|
|
||||||
: dailyCharacter.affiliations;
|
|
||||||
const charFirstAff = Array.isArray(charAff) ? charAff[0] : charAff;
|
|
||||||
const dailyFirstAff = Array.isArray(dailyAff) ? dailyAff[0] : dailyAff;
|
|
||||||
return charFirstAff && dailyFirstAff && charFirstAff === dailyFirstAff ? 'bg-emerald-600/90' : 'bg-red-900/60';
|
|
||||||
} catch (e) {
|
|
||||||
return 'bg-slate-950/60';
|
|
||||||
}
|
|
||||||
})()} p-2 flex items-center justify-center overflow-hidden">
|
|
||||||
{#if character.affiliations}
|
|
||||||
{@const parsedAffiliations = typeof character.affiliations === 'string'
|
|
||||||
? (character.affiliations.includes('[') ? JSON.parse(character.affiliations) : character.affiliations.split(',').map((a: string) => a.trim()))
|
|
||||||
: character.affiliations}
|
|
||||||
{#if Array.isArray(parsedAffiliations) && parsedAffiliations.length > 0}
|
|
||||||
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations[0]}</p>
|
|
||||||
{:else}
|
|
||||||
<p class="w-full text-sm font-bold text-white text-center whitespace-normal break-words leading-tight">{parsedAffiliations}</p>
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<p class="text-base font-bold text-slate-400 text-center">-</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Fruit -->
|
|
||||||
{#if columnVisibility.devilFruitType !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.devilFruitType === dailyCharacter.devilFruitType ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
|
||||||
{#if character.devilFruitType}
|
|
||||||
<p class="text-sm font-bold text-white text-center">{character.devilFruitType}</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-5xl font-bold text-white text-center">✕</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Haki -->
|
|
||||||
{#if columnVisibility.haki !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {(() => {
|
|
||||||
if (character.hakiObservation === dailyCharacter.hakiObservation && character.hakiArmament === dailyCharacter.hakiArmament && character.hakiConqueror === dailyCharacter.hakiConqueror) {
|
|
||||||
return 'bg-emerald-600/90';
|
|
||||||
} else if ((character.hakiObservation && dailyCharacter.hakiObservation) ||
|
|
||||||
(character.hakiArmament && dailyCharacter.hakiArmament) ||
|
|
||||||
(character.hakiConqueror && dailyCharacter.hakiConqueror)) {
|
|
||||||
return 'bg-yellow-600/80';
|
|
||||||
} else {
|
|
||||||
return 'bg-red-900/60';
|
|
||||||
}
|
|
||||||
})()} p-2 flex items-center justify-center">
|
|
||||||
<p class="text-2xl font-bold text-white text-center">
|
|
||||||
{#if character.hakiObservation}<span title="Haki de l'Observation">👁️</span>{/if}
|
|
||||||
{#if character.hakiArmament}<span title="Haki de l'Armement">🦾</span>{/if}
|
|
||||||
{#if character.hakiConqueror}<span title="Haki des Rois">👑</span>{/if}
|
|
||||||
{#if !character.hakiObservation && !character.hakiArmament && !character.hakiConqueror}
|
|
||||||
<span class="text-5xl">✕</span>
|
|
||||||
{/if}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Prime -->
|
|
||||||
{#if columnVisibility.bounty !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.bounty === dailyCharacter.bounty ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
|
||||||
{#if character.bounty != null && dailyCharacter.bounty != null && character.bounty !== dailyCharacter.bounty}
|
|
||||||
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
|
||||||
background-color: rgb(203, 213, 225);
|
|
||||||
clip-path: {character.bounty > dailyCharacter.bounty
|
|
||||||
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
|
||||||
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
|
||||||
"></div>
|
|
||||||
{/if}
|
|
||||||
{#if character.bounty != null}
|
|
||||||
<p class="text-sm font-bold text-white text-center relative z-10">{formatBounty(character.bounty)} ฿</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Taille -->
|
|
||||||
{#if columnVisibility.height !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.height === dailyCharacter.height ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
|
||||||
{#if character.height && dailyCharacter.height && character.height !== dailyCharacter.height}
|
|
||||||
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
|
||||||
background-color: rgb(203, 213, 225);
|
|
||||||
clip-path: {character.height > dailyCharacter.height
|
|
||||||
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
|
||||||
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
|
||||||
"></div>
|
|
||||||
{/if}
|
|
||||||
{#if character.height}
|
|
||||||
<p class="text-sm font-bold text-white text-center relative z-10">{character.height} m</p>
|
|
||||||
{:else}
|
|
||||||
<p class="text-sm font-bold text-white text-center relative z-10">Inconnue</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Origine -->
|
|
||||||
{#if columnVisibility.origin !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.origin === dailyCharacter.origin ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center">
|
|
||||||
<p class="text-sm font-bold text-white text-center">{character.origin || 'Inconnue'}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Arc -->
|
|
||||||
{#if columnVisibility.arc !== false}
|
|
||||||
<div class="w-24 h-24 shrink-0 rounded-lg border border-white/10 {character.arcName === dailyCharacter.arcName ? 'bg-emerald-600/90' : 'bg-red-900/60'} p-2 flex items-center justify-center relative overflow-hidden">
|
|
||||||
{#if character.arcName !== dailyCharacter.arcName && character.firstAppearance && dailyCharacter.firstAppearance && character.firstAppearance !== dailyCharacter.firstAppearance}
|
|
||||||
<div class="absolute w-full h-full opacity-30 pointer-events-none" style="
|
|
||||||
background-color: rgb(203, 213, 225);
|
|
||||||
clip-path: {character.firstAppearance > dailyCharacter.firstAppearance
|
|
||||||
? 'polygon(97% 60%,80% 60%,80% 5%,20% 5%,20% 60%,3% 60%,50% 95%)'
|
|
||||||
: 'polygon(97% 40%,80% 40%,80% 95%,20% 95%,20% 40%,3% 40%,50% 5%)'};
|
|
||||||
"></div>
|
|
||||||
{/if}
|
|
||||||
<p class="text-sm font-bold text-white text-center relative z-10">{character.arcName || 'Inconnu'}</p>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="mt-8 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur">
|
|
||||||
{#if yesterdayCharacter}
|
|
||||||
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
|
||||||
{#if yesterdayCharacter.pictureUrl}
|
|
||||||
<img
|
|
||||||
src={yesterdayCharacter.pictureUrl}
|
|
||||||
alt={yesterdayCharacter.name}
|
|
||||||
class="h-20 w-20 rounded-full border border-amber-200/40 object-cover"
|
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
|
||||||
Photo
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
|
||||||
<p class="mt-2 text-lg font-semibold text-white">{yesterdayCharacter.name}</p>
|
|
||||||
{#if yesterdayCharacter.epithets}
|
|
||||||
<p class="mt-1 text-sm text-slate-400">
|
|
||||||
{typeof yesterdayCharacter.epithets === 'string'
|
|
||||||
? JSON.parse(yesterdayCharacter.epithets).join(', ')
|
|
||||||
: (yesterdayCharacter.epithets as string[]).join(', ')}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={"https://onepiece.fandom.com/fr/wiki/" + yesterdayCharacter.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="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 sm:w-auto"
|
|
||||||
>
|
|
||||||
Voir la page
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="flex flex-col items-center gap-5 text-center sm:flex-row sm:text-left">
|
|
||||||
<div class="flex h-20 w-20 items-center justify-center rounded-full border border-amber-200/40 bg-slate-900/70 text-xs uppercase tracking-[0.25em] text-amber-100">
|
|
||||||
Photo
|
|
||||||
</div>
|
|
||||||
<div class="flex-1">
|
|
||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100">Personnage d'hier</p>
|
|
||||||
<p class="mt-2 text-lg font-semibold text-white">Aucun personnage</p>
|
|
||||||
<p class="mt-1 text-sm text-slate-200">Aucun personnage d'hier disponible</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { db } from '$lib/server/db';
|
|||||||
import { characterHistory } from '$lib/server/db/schema';
|
import { characterHistory } from '$lib/server/db/schema';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import { sql } from 'drizzle-orm';
|
import { sql } from 'drizzle-orm';
|
||||||
|
import { getDateKey } from '$lib/server/daily-character';
|
||||||
|
|
||||||
export async function POST({ request }) {
|
export async function POST({ request }) {
|
||||||
try {
|
try {
|
||||||
@@ -12,9 +13,7 @@ export async function POST({ request }) {
|
|||||||
return json({ error: 'Missing characterId' }, { status: 400 });
|
return json({ error: 'Missing characterId' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const today = new Date();
|
const todayDate = getDateKey(new Date());
|
||||||
today.setHours(0, 0, 0, 0);
|
|
||||||
const todayDate = today.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
// Increment the won counter for today's entry
|
// Increment the won counter for today's entry
|
||||||
await db
|
await db
|
||||||
|
|||||||
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
|
||||||
|
};
|
||||||
|
}
|
||||||
416
src/routes/(game)/infinite/+page.svelte
Normal file
416
src/routes/(game)/infinite/+page.svelte
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
<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> = {};
|
||||||
|
const columnDisplayNames: Record<string, string> = {
|
||||||
|
status: 'Statut',
|
||||||
|
gender: 'Genre',
|
||||||
|
affiliations: 'Affiliations',
|
||||||
|
devilFruitType: 'Fruit',
|
||||||
|
haki: 'Haki',
|
||||||
|
bounty: 'Prime',
|
||||||
|
height: 'Taille',
|
||||||
|
origin: 'Origine',
|
||||||
|
arc: 'Arc'
|
||||||
|
};
|
||||||
|
|
||||||
|
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}
|
||||||
|
<GuessHistoryTable
|
||||||
|
{selectedCharacters}
|
||||||
|
dailyCharacter={currentCharacter}
|
||||||
|
{columnVisibility}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 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="mb-3 flex items-center justify-between gap-3">
|
||||||
|
<h3 class="text-xs font-semibold uppercase tracking-[0.2em] text-amber-200">Colonnes</h3>
|
||||||
|
<p class="text-xs text-slate-400">
|
||||||
|
{Object.values(columnVisibility).filter(Boolean).length}/{Object.keys(columnVisibility).length}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{#each Object.entries(columnVisibility) as [column, isVisible] (column)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => toggleColumnVisibility(column)}
|
||||||
|
class="rounded-full border px-2.5 py-1 text-xs 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'}"
|
||||||
|
>
|
||||||
|
{columnDisplayNames[column] || column.replace(/([A-Z])/g, ' $1').trim()}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/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>
|
||||||
@@ -25,13 +25,13 @@
|
|||||||
<title>OnePieceDle - {isSignUp ? 'Inscription' : 'Connexion'}</title>
|
<title>OnePieceDle - {isSignUp ? 'Inscription' : 'Connexion'}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100">
|
<main class="relative min-h-[calc(100vh-5rem)] 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 bg-gradient-to-br from-slate-950/85 via-slate-900/60 to-slate-950/80"></div>
|
||||||
<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 mix-blend-screen opacity-20 bg-[radial-gradient(circle_at_top,rgba(255,215,84,0.35),transparent_55%)]"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div class="relative mx-auto flex min-h-screen w-full max-w-2xl flex-col items-center justify-center px-6 py-10">
|
<div class="relative mx-auto flex w-full max-w-2xl flex-col items-center justify-center px-6 py-10">
|
||||||
<div class="w-full space-y-8">
|
<div class="w-full space-y-8">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
@@ -49,11 +49,11 @@
|
|||||||
<title>Mon Profil - OnePieceDle</title>
|
<title>Mon Profil - OnePieceDle</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<main class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100">
|
<main class="relative min-h-[calc(100vh-5rem)] 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 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="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-2xl flex-col items-center px-6 py-4">
|
<div class="relative mx-auto flex w-full max-w-2xl flex-col items-center px-6 py-4">
|
||||||
<div class="w-full space-y-4">
|
<div class="w-full space-y-4">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user