feat: add daily fruit feature and related components
Build Docker Image / build (push) Successful in 2m8s
Build Docker Image / build (push) Successful in 2m8s
- Introduced a new daily fruit selection mechanism with associated database schema. - Created routes and components for displaying and interacting with daily fruit. - Implemented logic for tracking wins related to daily fruit and character guesses. - Added localization support for new daily fruit titles in English and French. - Enhanced the user experience with history tracking for guesses and wins. - Refactored existing character-related logic to accommodate new fruit functionality.
This commit is contained in:
+1
-1
@@ -269,7 +269,7 @@
|
||||
<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">
|
||||
{$t.game.daily.title}
|
||||
{$t.game.daily.characterTitle}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-amber-300">
|
||||
{data.winCount} {data.winCount > 1 ? $t.game.daily.winsPeoplePlural : $t.game.daily.winsPeopleSingular} {data.winCount > 1 ? $t.game.daily.winsVerbPlural : $t.game.daily.winsVerbSingular} {$t.game.daily.winsSuffix}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { getAllDevilFruitsFromCharacters, getOrCreateTodayFruit, getYesterdayFruit, getTodayFruitWinsCount } from '$lib/server/daily-fruit';
|
||||
import { getAllCharacters } from '$lib/server/daily-character';
|
||||
|
||||
export async function load(event) {
|
||||
const fruits = await getAllDevilFruitsFromCharacters();
|
||||
const dailyFruit = await getOrCreateTodayFruit();
|
||||
|
||||
if (!dailyFruit) {
|
||||
throw error(404, 'No daily fruit available. Please check if devil fruits are configured from characters.');
|
||||
}
|
||||
|
||||
const yesterdayFruit = await getYesterdayFruit(new Date());
|
||||
|
||||
// Load all characters for searching and filter only those with a devil fruit
|
||||
const allCharacters = await getAllCharacters();
|
||||
const characters = allCharacters.filter((c) => c.devilFruitId);
|
||||
|
||||
// Find the character tied to the chosen fruit among characters that have a devil fruit
|
||||
const dailyCharacter = characters.find((c) => c.devilFruitId === dailyFruit.id) ?? null;
|
||||
|
||||
const winCount = await getTodayFruitWinsCount(dailyFruit.id);
|
||||
|
||||
return {
|
||||
fruits,
|
||||
dailyFruit,
|
||||
dailyCharacter,
|
||||
yesterdayFruit,
|
||||
winCount,
|
||||
characters
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
|
||||
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
|
||||
import SimpleGuessHistory from '$lib/components/SimpleGuessHistory.svelte';
|
||||
import WinPanel from '$lib/components/WinPanel.svelte';
|
||||
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
||||
import { t } from '$lib/i18n';
|
||||
|
||||
export let data;
|
||||
|
||||
let selectedCharacters: CharacterWithRelations[] = [];
|
||||
let isLoaded = false;
|
||||
|
||||
// Load from localStorage on mount
|
||||
onMount(() => {
|
||||
const storedDailyCharacterId = localStorage.getItem('dailyFruitTargetCharacterId');
|
||||
const dailyCurrentCharacterId = data.dailyCharacter?.id;
|
||||
|
||||
if (storedDailyCharacterId && storedDailyCharacterId !== dailyCurrentCharacterId) {
|
||||
localStorage.removeItem('dailyFruitHistory');
|
||||
selectedCharacters = [];
|
||||
} else {
|
||||
const stored = localStorage.getItem('dailyFruitHistory');
|
||||
if (stored) {
|
||||
try {
|
||||
const storedIds = JSON.parse(stored);
|
||||
if (Array.isArray(storedIds)) {
|
||||
selectedCharacters = storedIds
|
||||
.map((id: string) => data.characters.find((c: CharacterWithRelations) => c.id === id))
|
||||
.filter((c: CharacterWithRelations | undefined): c is CharacterWithRelations => !!c);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse stored history', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dailyCurrentCharacterId) {
|
||||
localStorage.setItem('dailyFruitTargetCharacterId', dailyCurrentCharacterId);
|
||||
}
|
||||
|
||||
isLoaded = true;
|
||||
});
|
||||
|
||||
onDestroy(() => {});
|
||||
|
||||
$: if (isLoaded && selectedCharacters) {
|
||||
const ids = selectedCharacters.map(char => char.id);
|
||||
localStorage.setItem('dailyFruitHistory', JSON.stringify(ids));
|
||||
}
|
||||
|
||||
$: characters = data.characters || [];
|
||||
$: dailyCharacter = data.dailyCharacter;
|
||||
$: dailyFruit = data.dailyFruit;
|
||||
$: yesterdayFruit = data.yesterdayFruit;
|
||||
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter?.id);
|
||||
|
||||
function handleCharacterSelect(character: CharacterWithRelations) {
|
||||
selectCharacter(character);
|
||||
}
|
||||
|
||||
function selectCharacter(character: CharacterWithRelations) {
|
||||
selectedCharacters = [character, ...selectedCharacters];
|
||||
|
||||
// Check if player won
|
||||
if (character.id === dailyCharacter?.id) {
|
||||
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
|
||||
|
||||
fetch('/daily/fruit', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
characterId: dailyCharacter.id,
|
||||
tryCount: selectedCharacters.length,
|
||||
triedCharacterIds
|
||||
})
|
||||
}).catch(err => console.error('Failed to record win:', err));
|
||||
}
|
||||
}
|
||||
|
||||
function resetHistory() {
|
||||
selectedCharacters = [];
|
||||
localStorage.removeItem('dailyFruitHistory');
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t.game.daily.metaTitle}</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100">
|
||||
<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">
|
||||
{$t.game.daily.fruitTitle}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-amber-300">
|
||||
{data.winCount} {data.winCount > 1 ? $t.game.daily.winsPeoplePlural : $t.game.daily.winsPeopleSingular}
|
||||
</p>
|
||||
</div>
|
||||
{#if hasWon}
|
||||
<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"
|
||||
on:click={resetHistory}
|
||||
>
|
||||
{$t.game.daily.reset}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="max-w-2xl text-base text-slate-200 sm:text-lg">
|
||||
Trouvez la personne liée au fruit du jour.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="mt-8 rounded-2xl border border-amber-200/20 bg-amber-300/10 px-5 py-4 text-center">
|
||||
<p class="mt-2 text-center text-xl font-bold text-amber-50 sm:text-2xl">{dailyFruit?.name}</p>
|
||||
</div>
|
||||
|
||||
<section class="mt-6 grid gap-6">
|
||||
{#if hasWon}
|
||||
<WinPanel
|
||||
selectedCharacter={dailyCharacter!}
|
||||
{selectedCharacters}
|
||||
/>
|
||||
{:else}
|
||||
<CharacterSearchInput
|
||||
{characters}
|
||||
{selectedCharacters}
|
||||
onSelect={handleCharacterSelect}
|
||||
/>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<SimpleGuessHistory {selectedCharacters} />
|
||||
|
||||
<YesterdayCharacter yesterdayCharacter={null} />
|
||||
</div>
|
||||
</main>
|
||||
@@ -0,0 +1,32 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { db } from '$lib/server/db';
|
||||
import { devilFruitHistory } from '$lib/server/db/schema';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { getDateKey } from '$lib/server/daily-fruit';
|
||||
|
||||
export async function POST({ request, locals }) {
|
||||
try {
|
||||
const { characterId, tryCount, triedCharacterIds } = await request.json();
|
||||
|
||||
if (!characterId) {
|
||||
return json({ error: 'Missing characterId' }, { status: 400 });
|
||||
}
|
||||
|
||||
const todayDate = getDateKey(new Date());
|
||||
|
||||
// Increment the won counter for today's devil fruit entry
|
||||
await db
|
||||
.update(devilFruitHistory)
|
||||
.set({
|
||||
won: sql`${devilFruitHistory.won} + 1`,
|
||||
updatedAt: Date.now()
|
||||
})
|
||||
.where(eq(devilFruitHistory.date, todayDate));
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error recording fruit win:', error);
|
||||
return json({ error: 'Failed to record win' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user