feat: add tried characters tracking and display in daily game profile
All checks were successful
Build Docker Image / build (push) Successful in 1m10s
All checks were successful
Build Docker Image / build (push) Successful in 1m10s
This commit is contained in:
@@ -88,6 +88,8 @@
|
|||||||
"changePassword": "Change password",
|
"changePassword": "Change password",
|
||||||
"dailyHistoryTitle": "Daily history",
|
"dailyHistoryTitle": "Daily history",
|
||||||
"noDailyHistory": "No history available",
|
"noDailyHistory": "No history available",
|
||||||
|
"triedCharactersTitle": "Tried characters",
|
||||||
|
"noTriedCharacters": "No characters recorded",
|
||||||
"noImage": "N/A",
|
"noImage": "N/A",
|
||||||
"trySingular": "try",
|
"trySingular": "try",
|
||||||
"tryPlural": "tries",
|
"tryPlural": "tries",
|
||||||
@@ -111,6 +113,8 @@
|
|||||||
"reset": "Play again",
|
"reset": "Play again",
|
||||||
"description": "Guess the character. Each hint unlocks after a certain number of guesses. Good luck!",
|
"description": "Guess the character. Each hint unlocks after a certain number of guesses. Good luck!",
|
||||||
"friendsToday": "Your friends today",
|
"friendsToday": "Your friends today",
|
||||||
|
"friendsTriedCharacters": "Tried characters",
|
||||||
|
"friendsNoTriedCharacters": "No characters recorded",
|
||||||
"friendTrySingular": "try",
|
"friendTrySingular": "try",
|
||||||
"friendTryPlural": "tries"
|
"friendTryPlural": "tries"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -88,6 +88,8 @@
|
|||||||
"changePassword": "Changer le mot de passe",
|
"changePassword": "Changer le mot de passe",
|
||||||
"dailyHistoryTitle": "Historique des Daily",
|
"dailyHistoryTitle": "Historique des Daily",
|
||||||
"noDailyHistory": "Aucun historique disponible",
|
"noDailyHistory": "Aucun historique disponible",
|
||||||
|
"triedCharactersTitle": "Personnages essayes",
|
||||||
|
"noTriedCharacters": "Aucun personnage enregistre",
|
||||||
"noImage": "N/A",
|
"noImage": "N/A",
|
||||||
"trySingular": "tentative",
|
"trySingular": "tentative",
|
||||||
"tryPlural": "tentatives",
|
"tryPlural": "tentatives",
|
||||||
@@ -111,6 +113,8 @@
|
|||||||
"reset": "Recommencer",
|
"reset": "Recommencer",
|
||||||
"description": "Devine le personnage. Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
|
"description": "Devine le personnage. Chaque indice se debloque apres un certain nombre de tentatives. Bonne chance !",
|
||||||
"friendsToday": "Tes amis aujourd'hui",
|
"friendsToday": "Tes amis aujourd'hui",
|
||||||
|
"friendsTriedCharacters": "Personnages essayes",
|
||||||
|
"friendsNoTriedCharacters": "Aucun personnage enregistre",
|
||||||
"friendTrySingular": "coup",
|
"friendTrySingular": "coup",
|
||||||
"friendTryPlural": "coups"
|
"friendTryPlural": "coups"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { error } from '@sveltejs/kit';
|
import { error } from '@sveltejs/kit';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { characterHistory, config, friendship, user, userCharacterHistory } from '$lib/server/db/schema';
|
import { character, characterHistory, config, friendship, user, userCharacterHistory } from '$lib/server/db/schema';
|
||||||
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter, getTodayCharacterWinsCount, getDateKey } from '$lib/server/daily-character';
|
import { getDailyModeCharacters, getOrCreateTodayCharacter, getYesterdayCharacter, getTodayCharacterWinsCount, getDateKey } from '$lib/server/daily-character';
|
||||||
import { and, eq, inArray, like, or } from 'drizzle-orm';
|
import { and, eq, inArray, like, or } from 'drizzle-orm';
|
||||||
|
|
||||||
@@ -17,7 +17,13 @@ export async function load(event) {
|
|||||||
// Load the win count for today
|
// Load the win count for today
|
||||||
const winCount = await getTodayCharacterWinsCount(dailyCharacter.id);
|
const winCount = await getTodayCharacterWinsCount(dailyCharacter.id);
|
||||||
|
|
||||||
let friendsTodayResults: Array<{ userId: string; name: string; image: string | null; tryCount: number }> = [];
|
let friendsTodayResults: Array<{
|
||||||
|
userId: string;
|
||||||
|
name: string;
|
||||||
|
image: string | null;
|
||||||
|
tryCount: number;
|
||||||
|
triedCharacters: Array<{ id: string; name: string; pictureUrl: string | null }>;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
const currentUserId = event.locals.user.id;
|
const currentUserId = event.locals.user.id;
|
||||||
@@ -51,12 +57,13 @@ export async function load(event) {
|
|||||||
const todayCharacterHistoryId = todayHistoryEntry?.id;
|
const todayCharacterHistoryId = todayHistoryEntry?.id;
|
||||||
|
|
||||||
if (todayCharacterHistoryId) {
|
if (todayCharacterHistoryId) {
|
||||||
friendsTodayResults = await db
|
const friendResultsRaw = await db
|
||||||
.select({
|
.select({
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
image: user.image,
|
image: user.image,
|
||||||
tryCount: userCharacterHistory.tryCount
|
tryCount: userCharacterHistory.tryCount,
|
||||||
|
triedCharacterIds: userCharacterHistory.triedCharacterIds
|
||||||
})
|
})
|
||||||
.from(userCharacterHistory)
|
.from(userCharacterHistory)
|
||||||
.innerJoin(user, eq(userCharacterHistory.userId, user.id))
|
.innerJoin(user, eq(userCharacterHistory.userId, user.id))
|
||||||
@@ -67,6 +74,33 @@ export async function load(event) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
.orderBy(userCharacterHistory.tryCount);
|
.orderBy(userCharacterHistory.tryCount);
|
||||||
|
|
||||||
|
const uniqueTriedCharacterIds = Array.from(new Set(
|
||||||
|
friendResultsRaw.flatMap((entry) => entry.triedCharacterIds ?? [])
|
||||||
|
));
|
||||||
|
|
||||||
|
const triedCharacters = uniqueTriedCharacterIds.length > 0
|
||||||
|
? await db
|
||||||
|
.select({
|
||||||
|
id: character.id,
|
||||||
|
name: character.name,
|
||||||
|
pictureUrl: character.pictureUrl
|
||||||
|
})
|
||||||
|
.from(character)
|
||||||
|
.where(inArray(character.id, uniqueTriedCharacterIds))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const triedCharactersById = new Map(triedCharacters.map((entry) => [entry.id, entry]));
|
||||||
|
|
||||||
|
friendsTodayResults = friendResultsRaw.map((entry) => ({
|
||||||
|
userId: entry.userId,
|
||||||
|
name: entry.name,
|
||||||
|
image: entry.image,
|
||||||
|
tryCount: entry.tryCount,
|
||||||
|
triedCharacters: (entry.triedCharacterIds ?? [])
|
||||||
|
.map((characterId) => triedCharactersById.get(characterId))
|
||||||
|
.filter((triedEntry): triedEntry is (typeof triedCharacters)[number] => !!triedEntry)
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,6 +159,8 @@
|
|||||||
|
|
||||||
// Check if player won
|
// Check if player won
|
||||||
if (character.id === dailyCharacter.id) {
|
if (character.id === dailyCharacter.id) {
|
||||||
|
const triedCharacterIds = selectedCharacters.map(selected => selected.id);
|
||||||
|
|
||||||
// Send request to record win in database
|
// Send request to record win in database
|
||||||
fetch('/daily', {
|
fetch('/daily', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -167,7 +169,8 @@
|
|||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
characterId: dailyCharacter.id,
|
characterId: dailyCharacter.id,
|
||||||
tryCount: selectedCharacters.length
|
tryCount: selectedCharacters.length,
|
||||||
|
triedCharacterIds
|
||||||
})
|
})
|
||||||
}).catch(err => console.error('Failed to record win:', err));
|
}).catch(err => console.error('Failed to record win:', err));
|
||||||
|
|
||||||
@@ -317,7 +320,8 @@
|
|||||||
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">{$t.game.daily.friendsToday}</p>
|
<p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">{$t.game.daily.friendsToday}</p>
|
||||||
<div class="mt-4 space-y-2">
|
<div class="mt-4 space-y-2">
|
||||||
{#each data.friendsTodayResults as friendResult (friendResult.userId)}
|
{#each data.friendsTodayResults as friendResult (friendResult.userId)}
|
||||||
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-slate-950/50 px-4 py-2">
|
<div class="rounded-lg border border-white/10 bg-slate-950/50 px-4 py-3">
|
||||||
|
<div class="flex items-center justify-between gap-4">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
{#if friendResult.image}
|
{#if friendResult.image}
|
||||||
<img
|
<img
|
||||||
@@ -336,6 +340,30 @@
|
|||||||
{friendResult.tryCount} {friendResult.tryCount > 1 ? $t.game.daily.friendTryPlural : $t.game.daily.friendTrySingular}
|
{friendResult.tryCount} {friendResult.tryCount > 1 ? $t.game.daily.friendTryPlural : $t.game.daily.friendTrySingular}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-3 border-t border-white/10 pt-2">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||||
|
{$t.game.daily.friendsTriedCharacters}
|
||||||
|
</p>
|
||||||
|
{#if friendResult.triedCharacters && friendResult.triedCharacters.length > 0}
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
{#each friendResult.triedCharacters as triedCharacter (triedCharacter.id)}
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-2.5 py-1 text-xs text-slate-200">
|
||||||
|
{#if triedCharacter.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={triedCharacter.pictureUrl}
|
||||||
|
alt={triedCharacter.name}
|
||||||
|
class="h-4 w-4 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{triedCharacter.name}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{$t.game.daily.friendsNoTriedCharacters}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ import { getDateKey } from '$lib/server/daily-character';
|
|||||||
|
|
||||||
export async function POST({ request, locals }) {
|
export async function POST({ request, locals }) {
|
||||||
try {
|
try {
|
||||||
const { characterId, tryCount } = await request.json();
|
const { characterId, tryCount, triedCharacterIds } = await request.json();
|
||||||
|
const normalizedTriedCharacterIds = Array.isArray(triedCharacterIds)
|
||||||
|
? triedCharacterIds.filter((id): id is string => typeof id === 'string')
|
||||||
|
: [];
|
||||||
|
|
||||||
if (!characterId) {
|
if (!characterId) {
|
||||||
return json({ error: 'Missing characterId' }, { status: 400 });
|
return json({ error: 'Missing characterId' }, { status: 400 });
|
||||||
@@ -51,7 +54,8 @@ export async function POST({ request, locals }) {
|
|||||||
await db.insert(userCharacterHistory).values({
|
await db.insert(userCharacterHistory).values({
|
||||||
userId: locals.user.id,
|
userId: locals.user.id,
|
||||||
characterHistoryId: todayHistoryEntry.id,
|
characterHistoryId: todayHistoryEntry.id,
|
||||||
tryCount: tryCount
|
tryCount: tryCount,
|
||||||
|
triedCharacterIds: normalizedTriedCharacterIds
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Actions, PageServerLoad } from './$types';
|
|||||||
import { auth } from '$lib/server/auth';
|
import { auth } from '$lib/server/auth';
|
||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema';
|
import { session, userCharacterHistory, characterHistory, character, friendship, user } from '$lib/server/db/schema';
|
||||||
import { and, desc, eq, or, sql } from 'drizzle-orm';
|
import { and, desc, eq, inArray, or, sql } from 'drizzle-orm';
|
||||||
import { APIError } from 'better-auth/api';
|
import { APIError } from 'better-auth/api';
|
||||||
|
|
||||||
export const load: PageServerLoad = async (event) => {
|
export const load: PageServerLoad = async (event) => {
|
||||||
@@ -20,12 +20,13 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
.where(eq(session.userId, event.locals.user.id));
|
.where(eq(session.userId, event.locals.user.id));
|
||||||
|
|
||||||
// Fetch daily history for this user
|
// Fetch daily history for this user
|
||||||
const dailyHistory = await db
|
const dailyHistoryRaw = await db
|
||||||
.select({
|
.select({
|
||||||
id: userCharacterHistory.id,
|
id: userCharacterHistory.id,
|
||||||
characterId: characterHistory.characterId,
|
characterId: characterHistory.characterId,
|
||||||
date: characterHistory.date,
|
date: characterHistory.date,
|
||||||
tryCount: userCharacterHistory.tryCount,
|
tryCount: userCharacterHistory.tryCount,
|
||||||
|
triedCharacterIds: userCharacterHistory.triedCharacterIds,
|
||||||
won: characterHistory.won,
|
won: characterHistory.won,
|
||||||
characterName: character.name,
|
characterName: character.name,
|
||||||
characterImage: character.pictureUrl
|
characterImage: character.pictureUrl
|
||||||
@@ -36,6 +37,30 @@ export const load: PageServerLoad = async (event) => {
|
|||||||
.where(eq(userCharacterHistory.userId, event.locals.user.id))
|
.where(eq(userCharacterHistory.userId, event.locals.user.id))
|
||||||
.orderBy(desc(characterHistory.date));
|
.orderBy(desc(characterHistory.date));
|
||||||
|
|
||||||
|
const uniqueTriedCharacterIds = Array.from(new Set(
|
||||||
|
dailyHistoryRaw.flatMap((entry) => entry.triedCharacterIds ?? [])
|
||||||
|
));
|
||||||
|
|
||||||
|
const triedCharacters = uniqueTriedCharacterIds.length > 0
|
||||||
|
? await db
|
||||||
|
.select({
|
||||||
|
id: character.id,
|
||||||
|
name: character.name,
|
||||||
|
pictureUrl: character.pictureUrl
|
||||||
|
})
|
||||||
|
.from(character)
|
||||||
|
.where(inArray(character.id, uniqueTriedCharacterIds))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const triedCharactersById = new Map(triedCharacters.map((entry) => [entry.id, entry]));
|
||||||
|
|
||||||
|
const dailyHistory = dailyHistoryRaw.map((entry) => ({
|
||||||
|
...entry,
|
||||||
|
triedCharacters: (entry.triedCharacterIds ?? [])
|
||||||
|
.map((characterId) => triedCharactersById.get(characterId))
|
||||||
|
.filter((triedEntry): triedEntry is (typeof triedCharacters)[number] => !!triedEntry)
|
||||||
|
}));
|
||||||
|
|
||||||
const incomingRequests = await db
|
const incomingRequests = await db
|
||||||
.select({
|
.select({
|
||||||
id: friendship.id,
|
id: friendship.id,
|
||||||
|
|||||||
@@ -9,6 +9,21 @@
|
|||||||
form?: { success?: boolean; message?: string } | null;
|
form?: { success?: boolean; message?: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DailyHistoryEntry {
|
||||||
|
id: string;
|
||||||
|
characterId: string | null;
|
||||||
|
date: number;
|
||||||
|
tryCount: number;
|
||||||
|
won: number;
|
||||||
|
characterName: string;
|
||||||
|
characterImage: string | null;
|
||||||
|
triedCharacters?: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
pictureUrl: string | null;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
let { data, form }: Props = $props();
|
let { data, form }: Props = $props();
|
||||||
|
|
||||||
let isLoading = $state(false);
|
let isLoading = $state(false);
|
||||||
@@ -20,7 +35,7 @@
|
|||||||
let newPassword = $state('');
|
let newPassword = $state('');
|
||||||
let confirmPassword = $state('');
|
let confirmPassword = $state('');
|
||||||
let sessions = $derived(data.sessions || []);
|
let sessions = $derived(data.sessions || []);
|
||||||
let dailyHistory = $derived(data.dailyHistory || []);
|
let dailyHistory = $derived((data.dailyHistory || []) as DailyHistoryEntry[]);
|
||||||
let friends = $derived(data.friends || []);
|
let friends = $derived(data.friends || []);
|
||||||
let incomingRequests = $derived(data.incomingRequests || []);
|
let incomingRequests = $derived(data.incomingRequests || []);
|
||||||
let outgoingRequests = $derived(data.outgoingRequests || []);
|
let outgoingRequests = $derived(data.outgoingRequests || []);
|
||||||
@@ -455,6 +470,29 @@
|
|||||||
day: 'numeric'
|
day: 'numeric'
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-[11px] font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||||
|
{$t.game.profile.triedCharactersTitle}
|
||||||
|
</p>
|
||||||
|
{#if day.triedCharacters && day.triedCharacters.length > 0}
|
||||||
|
<div class="mt-2 flex flex-wrap gap-2">
|
||||||
|
{#each day.triedCharacters as triedCharacter (triedCharacter.id)}
|
||||||
|
<span class="inline-flex items-center gap-2 rounded-full border border-white/15 bg-white/5 px-2.5 py-1 text-xs text-slate-200">
|
||||||
|
{#if triedCharacter.pictureUrl}
|
||||||
|
<img
|
||||||
|
src={triedCharacter.pictureUrl}
|
||||||
|
alt={triedCharacter.name}
|
||||||
|
class="h-4 w-4 rounded-full object-cover"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{triedCharacter.name}
|
||||||
|
</span>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p class="mt-1 text-xs text-slate-500">{$t.game.profile.noTriedCharacters}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tries -->
|
<!-- Tries -->
|
||||||
|
|||||||
Reference in New Issue
Block a user