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:
@@ -0,0 +1,11 @@
|
|||||||
|
CREATE TABLE `devil_fruit_history` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`devil_fruit_id` text,
|
||||||
|
`date` integer NOT NULL,
|
||||||
|
`won` integer DEFAULT 0 NOT NULL,
|
||||||
|
`created_at` integer NOT NULL,
|
||||||
|
`updated_at` integer NOT NULL,
|
||||||
|
FOREIGN KEY (`devil_fruit_id`) REFERENCES `devil_fruit`(`id`) ON UPDATE no action ON DELETE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX `devil_fruit_history_date_unique` ON `devil_fruit_history` (`date`);
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -29,6 +29,13 @@
|
|||||||
"when": 1776195681488,
|
"when": 1776195681488,
|
||||||
"tag": "0003_mixed_ben_grimm",
|
"tag": "0003_mixed_ben_grimm",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 4,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1782152097346,
|
||||||
|
"tag": "0004_magenta_screwball",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { CharacterWithRelations } from '$lib/server/daily-character';
|
||||||
|
export let selectedCharacters: CharacterWithRelations[] = [];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div 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 sm:p-8">
|
||||||
|
<h2 class="text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">Historique</h2>
|
||||||
|
|
||||||
|
{#if selectedCharacters.length === 0}
|
||||||
|
<p class="mt-4 text-sm text-slate-400">Aucun personnage sélectionné</p>
|
||||||
|
{:else}
|
||||||
|
<ol class="mt-6 divide-y divide-white/10">
|
||||||
|
{#each selectedCharacters as char, index (char.id)}
|
||||||
|
<li class="flex items-center gap-4 py-4 sm:py-5">
|
||||||
|
<span class="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-amber-200/30 bg-slate-900/70 text-sm font-semibold text-amber-100">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{#if char.pictureUrl}
|
||||||
|
<img src={char.pictureUrl} alt={char.name} class="h-12 w-12 rounded-full object-cover border border-amber-200/30" />
|
||||||
|
{:else}
|
||||||
|
<div class="flex h-12 w-12 items-center justify-center rounded-full border border-amber-200/30 bg-slate-800 text-xs text-slate-400">?</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<span class="text-base font-medium text-slate-100">{char.name}</span>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ol>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
@@ -104,7 +104,8 @@
|
|||||||
},
|
},
|
||||||
"daily": {
|
"daily": {
|
||||||
"metaTitle": "OnePieceDle - Daily Mode",
|
"metaTitle": "OnePieceDle - Daily Mode",
|
||||||
"title": "Daily Character",
|
"characterTitle": "daily - character",
|
||||||
|
"fruitTitle": "daily - fruit",
|
||||||
"winsPeopleSingular": "person",
|
"winsPeopleSingular": "person",
|
||||||
"winsPeoplePlural": "people",
|
"winsPeoplePlural": "people",
|
||||||
"winsVerbSingular": "has",
|
"winsVerbSingular": "has",
|
||||||
|
|||||||
@@ -104,7 +104,8 @@
|
|||||||
},
|
},
|
||||||
"daily": {
|
"daily": {
|
||||||
"metaTitle": "OnePieceDle - Mode du jour",
|
"metaTitle": "OnePieceDle - Mode du jour",
|
||||||
"title": "Personnage du jour",
|
"characterTitle": "daily - personnage",
|
||||||
|
"fruitTitle": "daily - fruit",
|
||||||
"winsPeopleSingular": "personne",
|
"winsPeopleSingular": "personne",
|
||||||
"winsPeoplePlural": "personnes",
|
"winsPeoplePlural": "personnes",
|
||||||
"winsVerbSingular": "a",
|
"winsVerbSingular": "a",
|
||||||
|
|||||||
@@ -42,15 +42,6 @@ export type CharacterWithRelations = Character & {
|
|||||||
frArcName: string | null;
|
frArcName: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type RelationMaps = {
|
|
||||||
arcNameById: Map<string, string | null>;
|
|
||||||
devilFruitById: Map<string, { name: string | null; type: string | null }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isNotNullish<T>(value: T | null | undefined): value is T {
|
|
||||||
return value !== null && value !== undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateKey(date: Date): number {
|
export function getDateKey(date: Date): number {
|
||||||
return normalizeDay(date).getTime();
|
return normalizeDay(date).getTime();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { character, devilFruit, devilFruitHistory, type DevilFruit, type Character } from '$lib/server/db/schema';
|
||||||
|
import { desc, eq, and } from 'drizzle-orm';
|
||||||
|
|
||||||
|
// Generate or get random seed for daily fruit selection
|
||||||
|
const RANDOM_SEED = Math.random();
|
||||||
|
|
||||||
|
const characterWithFruitSelect = {
|
||||||
|
id: character.id,
|
||||||
|
name: character.name,
|
||||||
|
devilFruitId: character.devilFruitId,
|
||||||
|
devilFruitName: devilFruit.name,
|
||||||
|
devilFruitType: devilFruit.type,
|
||||||
|
// include url if available from devilFruit join
|
||||||
|
url: devilFruit.url
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CharacterWithFruit = Character & {
|
||||||
|
devilFruitName: string | null;
|
||||||
|
devilFruitType: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getDateKey(date: Date): number {
|
||||||
|
return normalizeDay(date).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeDay(date: Date = new Date()): Date {
|
||||||
|
const normalized = new Date(date);
|
||||||
|
normalized.setHours(1, 0, 0, 0);
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickDailyFruit(fruits: DevilFruit[], date: Date): DevilFruit {
|
||||||
|
const timestamp = getDateKey(date);
|
||||||
|
const daysSinceEpoch = Math.floor(timestamp / 1000 / 60 / 60 / 24);
|
||||||
|
const combinedSeed = (daysSinceEpoch + Math.floor(RANDOM_SEED * 1000000)) % fruits.length;
|
||||||
|
return fruits[combinedSeed];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCharactersWithDevilFruit(): Promise<CharacterWithFruit[]> {
|
||||||
|
const rows = (await db
|
||||||
|
.select(characterWithFruitSelect)
|
||||||
|
.from(character)
|
||||||
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
|
.all()) as any[];
|
||||||
|
|
||||||
|
return rows.filter((r) => r.devilFruitId) as CharacterWithFruit[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllDevilFruitsFromCharacters(): Promise<DevilFruit[]> {
|
||||||
|
const characters = (await db
|
||||||
|
.select(characterWithFruitSelect)
|
||||||
|
.from(character)
|
||||||
|
.leftJoin(devilFruit, eq(character.devilFruitId, devilFruit.id))
|
||||||
|
.all()) as any[];
|
||||||
|
|
||||||
|
const map = new Map<string, DevilFruit>();
|
||||||
|
for (const row of characters) {
|
||||||
|
if (row.devilFruitId && row.devilFruitName) {
|
||||||
|
if (!map.has(row.devilFruitId)) {
|
||||||
|
map.set(row.devilFruitId, {
|
||||||
|
id: row.devilFruitId,
|
||||||
|
name: row.devilFruitName,
|
||||||
|
type: row.devilFruitType ?? 'Unknown',
|
||||||
|
url: row.url ?? null
|
||||||
|
} as DevilFruit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getFruitById(fruitId: string): Promise<DevilFruit | null> {
|
||||||
|
const [found] = await db.select().from(devilFruit).where(eq(devilFruit.id, fruitId)).limit(1);
|
||||||
|
return (found as DevilFruit) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrCreateTodayFruit(date: Date = new Date()): Promise<DevilFruit | null> {
|
||||||
|
const today = normalizeDay(date);
|
||||||
|
const todayDate = getDateKey(today);
|
||||||
|
|
||||||
|
const [existingEntry] = await db
|
||||||
|
.select()
|
||||||
|
.from(devilFruitHistory)
|
||||||
|
.where(eq(devilFruitHistory.date, todayDate))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existingEntry?.devilFruitId) {
|
||||||
|
return getFruitById(existingEntry.devilFruitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFruits = await getAllDevilFruitsFromCharacters();
|
||||||
|
if (allFruits.length === 0) return null;
|
||||||
|
|
||||||
|
const chosen = pickDailyFruit(allFruits, today);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.insert(devilFruitHistory).values({
|
||||||
|
devilFruitId: chosen.id,
|
||||||
|
date: todayDate,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now()
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to record daily fruit:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chosen;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getYesterdayFruit(date: Date = new Date()): Promise<DevilFruit | null> {
|
||||||
|
const yesterday = new Date(date);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
const yesterdayDate = getDateKey(yesterday);
|
||||||
|
|
||||||
|
const [yesterdayEntry] = await db
|
||||||
|
.select()
|
||||||
|
.from(devilFruitHistory)
|
||||||
|
.where(eq(devilFruitHistory.date, yesterdayDate))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!yesterdayEntry?.devilFruitId) return null;
|
||||||
|
|
||||||
|
return getFruitById(yesterdayEntry.devilFruitId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTodayFruitWinsCount(fruitId: string, date: Date = new Date()): Promise<number> {
|
||||||
|
const today = normalizeDay(date);
|
||||||
|
const todayDate = getDateKey(today);
|
||||||
|
|
||||||
|
const [result] = await db
|
||||||
|
.select({ won: devilFruitHistory.won })
|
||||||
|
.from(devilFruitHistory)
|
||||||
|
.where(and(eq(devilFruitHistory.devilFruitId, fruitId), eq(devilFruitHistory.date, todayDate)));
|
||||||
|
|
||||||
|
return result?.won ?? 0;
|
||||||
|
}
|
||||||
@@ -126,6 +126,20 @@ export const userCharacterHistory = sqliteTable('user_character_history', {
|
|||||||
|
|
||||||
export type UserCharacterHistory = InferSelectModel<typeof userCharacterHistory>;
|
export type UserCharacterHistory = InferSelectModel<typeof userCharacterHistory>;
|
||||||
|
|
||||||
|
// Define the devil fruit history table schema
|
||||||
|
export const devilFruitHistory = sqliteTable('devil_fruit_history', {
|
||||||
|
id: text('id')
|
||||||
|
.primaryKey()
|
||||||
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
|
devilFruitId: text('devil_fruit_id').references(() => devilFruit.id, { onDelete: 'cascade' }),
|
||||||
|
date: integer('date').notNull().unique(),
|
||||||
|
won: integer('won').notNull().default(0),
|
||||||
|
createdAt: integer('created_at').notNull().$default(() => Date.now()),
|
||||||
|
updatedAt: integer('updated_at').notNull().$default(() => Date.now()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type DevilFruitHistory = InferSelectModel<typeof devilFruitHistory>;
|
||||||
|
|
||||||
// Define the friendship table schema (friend requests + accepted friends)
|
// Define the friendship table schema (friend requests + accepted friends)
|
||||||
export const friendship = sqliteTable('friendship', {
|
export const friendship = sqliteTable('friendship', {
|
||||||
id: text('id')
|
id: text('id')
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<p class="mt-3 text-lg font-semibold text-white">{$t.game.home.dailySubtitle}</p>
|
<p class="mt-3 text-lg font-semibold text-white">{$t.game.home.dailySubtitle}</p>
|
||||||
<p class="mt-2 text-sm text-slate-200">{$t.game.home.dailyDescription}</p>
|
<p class="mt-2 text-sm text-slate-200">{$t.game.home.dailyDescription}</p>
|
||||||
<a
|
<a
|
||||||
href={resolve("/daily")}
|
href={resolve("/daily/character")}
|
||||||
class="mt-5 inline-flex w-full items-center justify-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200"
|
class="mt-5 inline-flex w-full items-center justify-center rounded-full bg-amber-300 px-5 py-3 text-sm font-semibold text-slate-900 transition hover:bg-amber-200"
|
||||||
>
|
>
|
||||||
{$t.game.home.dailyCta}
|
{$t.game.home.dailyCta}
|
||||||
|
|||||||
+1
-1
@@ -269,7 +269,7 @@
|
|||||||
<div class="flex w-full items-center justify-between gap-4">
|
<div class="flex w-full items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-3xl font-black uppercase tracking-[0.25em] text-amber-50 sm:text-5xl">
|
<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>
|
</h1>
|
||||||
<p class="mt-2 text-sm text-amber-300">
|
<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}
|
{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