refactor: improve type definitions and enhance state management in profile and daily components

This commit is contained in:
2026-03-14 16:31:16 +01:00
parent 4e95abf09f
commit e5a21cb0af
5 changed files with 139 additions and 76 deletions

View File

@@ -1,5 +1,5 @@
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, type Character, type CharacterOverride } from '$lib/server/db/schema';
import { desc, eq, inArray, and } from 'drizzle-orm'; import { desc, eq, inArray, and } from 'drizzle-orm';
// Generate or get random seed for daily character selection // Generate or get random seed for daily character selection
@@ -29,14 +29,12 @@ const characterWithRelationsSelect = {
arcName: arc.name arcName: arc.name
}; };
export type CharacterWithRelations = typeof character.$inferSelect & { export type CharacterWithRelations = Character & {
devilFruitName: string | null; devilFruitName: string | null;
devilFruitType: string | null; devilFruitType: string | null;
arcName: string | null; arcName: string | null;
}; };
type CharacterOverrideRow = typeof characterOverride.$inferSelect;
type RelationMaps = { type RelationMaps = {
arcNameById: Map<string, string | null>; arcNameById: Map<string, string | null>;
devilFruitById: Map<string, { name: string | null; type: string | null }>; devilFruitById: Map<string, { name: string | null; type: string | null }>;
@@ -48,7 +46,7 @@ function isNotNullish<T>(value: T | null | undefined): value is T {
function mergeCharacterWithOverride( function mergeCharacterWithOverride(
baseCharacter: CharacterWithRelations, baseCharacter: CharacterWithRelations,
overrideRow?: CharacterOverrideRow, overrideRow?: CharacterOverride,
relationMaps?: RelationMaps relationMaps?: RelationMaps
): CharacterWithRelations { ): CharacterWithRelations {
if (!overrideRow) { if (!overrideRow) {
@@ -104,7 +102,7 @@ async function applyCharacterOverrides(
return characters; return characters;
} }
const overrideByCharacterId = new Map<string, CharacterOverrideRow>( const overrideByCharacterId = new Map<string, CharacterOverride>(
overrideRows.map((overrideRow) => [overrideRow.characterId, overrideRow]) overrideRows.map((overrideRow) => [overrideRow.characterId, overrideRow])
); );

View File

@@ -1,5 +1,6 @@
import { integer, sqliteTable, text, real, unique } from 'drizzle-orm/sqlite-core'; import { integer, sqliteTable, text, real, unique } from 'drizzle-orm/sqlite-core';
import { user } from './auth.schema'; import { user } from './auth.schema';
import type { InferSelectModel } from 'drizzle-orm';
// Define devil fruit types // Define devil fruit types
export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Smile' | 'Unknown'; export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Smile' | 'Unknown';
@@ -23,6 +24,8 @@ export const arc = sqliteTable('arc', {
url: text('url') url: text('url')
}); });
export type Arc = InferSelectModel<typeof arc>;
// Define the devil fruit table schema // Define the devil fruit table schema
export const devilFruit = sqliteTable('devil_fruit', { export const devilFruit = sqliteTable('devil_fruit', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
@@ -31,6 +34,8 @@ export const devilFruit = sqliteTable('devil_fruit', {
url: text('url') url: text('url')
}); });
export type DevilFruit = InferSelectModel<typeof devilFruit>;
// Define the character table schema // Define the character table schema
export const character = sqliteTable('character', { export const character = sqliteTable('character', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
@@ -58,6 +63,8 @@ export const character = sqliteTable('character', {
isInDailyMode: integer('is_in_daily_mode', { mode: 'boolean' }).default(false) isInDailyMode: integer('is_in_daily_mode', { mode: 'boolean' }).default(false)
}); });
export type Character = InferSelectModel<typeof character>;
// Define the character override table schema // Define the character override table schema
export const characterOverride = sqliteTable('character_override', { export const characterOverride = sqliteTable('character_override', {
characterId: text('character_id').primaryKey().references(() => character.id, { onDelete: 'cascade' }), characterId: text('character_id').primaryKey().references(() => character.id, { onDelete: 'cascade' }),
@@ -82,6 +89,8 @@ export const characterOverride = sqliteTable('character_override', {
notes: text('notes') notes: text('notes')
}); });
export type CharacterOverride = InferSelectModel<typeof characterOverride>;
// Define the character scrape validation table schema // Define the character scrape validation table schema
export const characterScrapeValidation = sqliteTable('character_scrape_validation', { export const characterScrapeValidation = sqliteTable('character_scrape_validation', {
id: text('id').primaryKey(), id: text('id').primaryKey(),
@@ -108,6 +117,8 @@ export const characterScrapeValidation = sqliteTable('character_scrape_validatio
frUrl: text('fr_url') frUrl: text('fr_url')
}); });
export type CharacterScrapeValidation = InferSelectModel<typeof characterScrapeValidation>;
// Define the character history table schema // Define the character history table schema
export const characterHistory = sqliteTable('character_history', { export const characterHistory = sqliteTable('character_history', {
id: text('id') id: text('id')
@@ -120,6 +131,8 @@ export const characterHistory = sqliteTable('character_history', {
updatedAt: integer('updated_at').notNull().$default(() => Date.now()), updatedAt: integer('updated_at').notNull().$default(() => Date.now()),
}); });
export type CharacterHistory = InferSelectModel<typeof characterHistory>;
// Define the user character history table schema // Define the user character history table schema
export const userCharacterHistory = sqliteTable('user_character_history', { export const userCharacterHistory = sqliteTable('user_character_history', {
id: text('id') id: text('id')
@@ -134,6 +147,8 @@ export const userCharacterHistory = sqliteTable('user_character_history', {
unique().on(table.userId, table.characterHistoryId) unique().on(table.userId, table.characterHistoryId)
]); ]);
export type UserCharacterHistory = InferSelectModel<typeof userCharacterHistory>;
// 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')
@@ -152,5 +167,6 @@ export const friendship = sqliteTable('friendship', {
unique().on(table.requesterId, table.addresseeId) unique().on(table.requesterId, table.addresseeId)
]); ]);
export type Friendship = InferSelectModel<typeof friendship>;
export * from './auth.schema'; export * from './auth.schema';

View File

@@ -1,23 +1,96 @@
<script lang="ts"> <script lang="ts">
import { onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte'; import YesterdayCharacter from '$lib/components/YesterdayCharacter.svelte';
import HintsPanel from '$lib/components/HintsPanel.svelte'; import HintsPanel from '$lib/components/HintsPanel.svelte';
import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte'; import CharacterSearchInput from '$lib/components/CharacterSearchInput.svelte';
import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte'; import GuessHistoryTable from '$lib/components/GuessHistoryTable.svelte';
import WinPanel from '$lib/components/WinPanel.svelte'; import WinPanel from '$lib/components/WinPanel.svelte';
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
export let data; export let data;
let selectedCharacters: any[] = []; let selectedCharacters: CharacterWithRelations[] = [];
let isLoaded = false; let isLoaded = false;
let isGeckoMoriaWin = false; let isGeckoMoriaWin = false;
let wasOriginAvailable = false;
let wasFruitAvailable = false;
let wasAffiliationAvailable = false;
let showOriginUnlock = false; let showOriginUnlock = false;
let showFruitUnlock = false; let showFruitUnlock = false;
let showAffiliationUnlock = false; let showAffiliationUnlock = false;
let originUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
let fruitUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
let affiliationUnlockTimeout: ReturnType<typeof setTimeout> | null = null;
function clearUnlockTimeout(timeout: ReturnType<typeof setTimeout> | null) {
if (timeout) {
clearTimeout(timeout);
}
}
function pulseUnlock(type: 'origin' | 'fruit' | 'affiliation') {
if (type === 'origin') {
clearUnlockTimeout(originUnlockTimeout);
showOriginUnlock = true;
originUnlockTimeout = setTimeout(() => {
showOriginUnlock = false;
originUnlockTimeout = null;
}, 600);
return;
}
if (type === 'fruit') {
clearUnlockTimeout(fruitUnlockTimeout);
showFruitUnlock = true;
fruitUnlockTimeout = setTimeout(() => {
showFruitUnlock = false;
fruitUnlockTimeout = null;
}, 600);
return;
}
clearUnlockTimeout(affiliationUnlockTimeout);
showAffiliationUnlock = true;
affiliationUnlockTimeout = setTimeout(() => {
showAffiliationUnlock = false;
affiliationUnlockTimeout = null;
}, 600);
}
function syncHintAvailability(previousGuessCount: number, nextGuessCount: number, animateUnlocks = false) {
const nextOriginAvailable = nextGuessCount >= 5;
const nextFruitAvailable = nextGuessCount >= 10;
const nextAffiliationAvailable = nextGuessCount >= 15;
if (animateUnlocks && nextOriginAvailable && previousGuessCount < 5) {
pulseUnlock('origin');
}
if (animateUnlocks && nextFruitAvailable && previousGuessCount < 10) {
pulseUnlock('fruit');
}
if (animateUnlocks && nextAffiliationAvailable && previousGuessCount < 15) {
pulseUnlock('affiliation');
}
if (!nextOriginAvailable) {
showOriginUnlock = false;
clearUnlockTimeout(originUnlockTimeout);
originUnlockTimeout = null;
}
if (!nextFruitAvailable) {
showFruitUnlock = false;
clearUnlockTimeout(fruitUnlockTimeout);
fruitUnlockTimeout = null;
}
if (!nextAffiliationAvailable) {
showAffiliationUnlock = false;
clearUnlockTimeout(affiliationUnlockTimeout);
affiliationUnlockTimeout = null;
}
}
// Load from localStorage on mount // Load from localStorage on mount
onMount(() => { onMount(() => {
@@ -37,8 +110,8 @@
// Reconstruct character objects from IDs // Reconstruct character objects from IDs
if (Array.isArray(storedIds)) { if (Array.isArray(storedIds)) {
selectedCharacters = storedIds selectedCharacters = storedIds
.map((id: string) => data.characters.find((c: any) => c.id === id)) .map((id: string) => data.characters.find((c: CharacterWithRelations) => c.id === id))
.filter((c: any) => c !== undefined); .filter((c: CharacterWithRelations | undefined): c is CharacterWithRelations => !!c);
} }
} catch (e) { } catch (e) {
console.error('Failed to parse stored history', e); console.error('Failed to parse stored history', e);
@@ -51,9 +124,17 @@
localStorage.setItem('dailyCurrentCharacterId', dailyCurrentCharacterId); localStorage.setItem('dailyCurrentCharacterId', dailyCurrentCharacterId);
} }
syncHintAvailability(0, selectedCharacters.length);
isLoaded = true; isLoaded = true;
}); });
onDestroy(() => {
clearUnlockTimeout(originUnlockTimeout);
clearUnlockTimeout(fruitUnlockTimeout);
clearUnlockTimeout(affiliationUnlockTimeout);
});
// Save to localStorage whenever selectedCharacters changes (only store IDs) // Save to localStorage whenever selectedCharacters changes (only store IDs)
$: if (isLoaded && selectedCharacters) { $: if (isLoaded && selectedCharacters) {
const ids = selectedCharacters.map(char => char.id); const ids = selectedCharacters.map(char => char.id);
@@ -66,39 +147,15 @@
$: columnVisibility = data.columnVisibility || {}; $: columnVisibility = data.columnVisibility || {};
$: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.id); $: hasWon = selectedCharacters.some(char => char.id === dailyCharacter.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 handleCharacterSelect(event: CustomEvent) { function handleCharacterSelect(event: CustomEvent) {
const character = event.detail; const character = event.detail;
selectCharacter(character); selectCharacter(character);
} }
function selectCharacter(character: any) { function selectCharacter(character: CharacterWithRelations) {
const previousGuessCount = selectedCharacters.length;
selectedCharacters = [character, ...selectedCharacters]; selectedCharacters = [character, ...selectedCharacters];
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
// Check if player won // Check if player won
if (character.id === dailyCharacter.id) { if (character.id === dailyCharacter.id) {
@@ -122,7 +179,9 @@
} }
function resetHistory() { function resetHistory() {
const previousGuessCount = selectedCharacters.length;
selectedCharacters = []; selectedCharacters = [];
syncHintAvailability(previousGuessCount, 0);
localStorage.removeItem('dailyCharacterHistory'); localStorage.removeItem('dailyCharacterHistory');
} }
</script> </script>
@@ -198,7 +257,7 @@
<main <main
class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin ? 'moria-screen-chaos' : ''}" class="relative min-h-screen overflow-hidden bg-slate-950 text-slate-100 {isGeckoMoriaWin ? 'moria-screen-chaos' : ''}"
> >
<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-linear-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 px-6 py-8 sm:py-10"> <div class="relative mx-auto flex min-h-screen w-full max-w-6xl flex-col px-6 py-8 sm:py-10">
@@ -257,7 +316,7 @@
<section class="mt-6 rounded-3xl border border-white/10 bg-white/5 p-6 shadow-[0_24px_60px_rgba(0,0,0,0.45)] backdrop-blur"> <section class="mt-6 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-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">Tes amis aujourd'hui</p> <p class="text-xs font-semibold uppercase tracking-[0.28em] text-amber-100 text-center">Tes amis aujourd'hui</p>
<div class="mt-4 space-y-2"> <div class="mt-4 space-y-2">
{#each data.friendsTodayResults as friendResult} {#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="flex items-center justify-between rounded-lg border border-white/10 bg-slate-950/50 px-4 py-2">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
{#if friendResult.image} {#if friendResult.image}

View File

@@ -190,6 +190,7 @@ export const actions: Actions = {
// Delete the session from database // Delete the session from database
await db.delete(session).where(eq(session.id, sessionId)); await db.delete(session).where(eq(session.id, sessionId));
} catch (error) { } catch (error) {
console.error('Error revoking session:', error);
return fail(500, { message: 'Erreur lors de la révocation de la session' }); return fail(500, { message: 'Erreur lors de la révocation de la session' });
} }

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { enhance } from '$app/forms'; import { enhance } from '$app/forms';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { resolve } from '$app/paths';
interface Props { interface Props {
data: PageData; data: PageData;
@@ -11,41 +12,29 @@
let isLoading = $state(false); let isLoading = $state(false);
let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile'); let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile');
let name = $state(''); let name = $derived(data.user?.name || '');
let friendUsername = $state(''); let friendUsername = $state('');
let showSuccess = $state(false); let showSuccess = $state(false);
let oldPassword = $state(''); let oldPassword = $state('');
let newPassword = $state(''); let newPassword = $state('');
let confirmPassword = $state(''); let confirmPassword = $state('');
let sessions = $state<any[]>([]); let sessions = $derived(data.sessions || []);
let dailyHistory = $state<any[]>([]); let dailyHistory = $derived(data.dailyHistory || []);
let friends = $state<any[]>([]); let friends = $derived(data.friends || []);
let incomingRequests = $state<any[]>([]); let incomingRequests = $derived(data.incomingRequests || []);
let outgoingRequests = $state<any[]>([]); let outgoingRequests = $derived(data.outgoingRequests || []);
let tabsElement: HTMLDivElement | undefined; let tabsElement: HTMLDivElement | undefined;
$effect(() => { $effect(() => {
name = data.user?.name || ''; friends = data.friends || [];
}); });
$effect(() => { $effect(() => {
sessions = (data as any).sessions || []; incomingRequests = data.incomingRequests || [];
}); });
$effect(() => { $effect(() => {
dailyHistory = (data as any).dailyHistory || []; outgoingRequests = data.outgoingRequests || [];
});
$effect(() => {
friends = (data as any).friends || [];
});
$effect(() => {
incomingRequests = (data as any).incomingRequests || [];
});
$effect(() => {
outgoingRequests = (data as any).outgoingRequests || [];
}); });
$effect(() => { $effect(() => {
@@ -71,7 +60,7 @@
</svelte:head> </svelte:head>
<main class="relative min-h-[calc(100vh-5rem)] 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-linear-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 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">
@@ -90,7 +79,7 @@
<div bind:this={tabsElement} class="sticky top-20 z-10 flex gap-2 border-b border-white/10 bg-slate-950/80 backdrop-blur"> <div bind:this={tabsElement} class="sticky top-20 z-10 flex gap-2 border-b border-white/10 bg-slate-950/80 backdrop-blur">
<button <button
onclick={() => handleTabChange('profile')} onclick={() => handleTabChange('profile')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'profile' class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'profile'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
@@ -98,7 +87,7 @@
</button> </button>
<button <button
onclick={() => handleTabChange('password')} onclick={() => handleTabChange('password')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'password' class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'password'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
@@ -106,7 +95,7 @@
</button> </button>
<button <button
onclick={() => handleTabChange('daily')} onclick={() => handleTabChange('daily')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'daily' class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'daily'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
@@ -114,7 +103,7 @@
</button> </button>
<button <button
onclick={() => handleTabChange('sessions')} onclick={() => handleTabChange('sessions')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'sessions' class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'sessions'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
@@ -122,7 +111,7 @@
</button> </button>
<button <button
onclick={() => handleTabChange('friends')} onclick={() => handleTabChange('friends')}
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'friends' class="px-4 py-3 font-semibold uppercase tracking-widest transition {activeTab === 'friends'
? 'border-b-2 border-amber-300 text-amber-100' ? 'border-b-2 border-amber-300 text-amber-100'
: 'text-slate-400 hover:text-slate-100'}" : 'text-slate-400 hover:text-slate-100'}"
> >
@@ -261,7 +250,7 @@
<p class="text-sm text-slate-400">Aucune demande reçue.</p> <p class="text-sm text-slate-400">Aucune demande reçue.</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each incomingRequests as req} {#each incomingRequests as req (req.id)}
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3"> <div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
<div> <div>
<p class="font-semibold text-white">{req.requesterName}</p> <p class="font-semibold text-white">{req.requesterName}</p>
@@ -289,7 +278,7 @@
<p class="text-sm text-slate-400">Aucune demande envoyée.</p> <p class="text-sm text-slate-400">Aucune demande envoyée.</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each outgoingRequests as req} {#each outgoingRequests as req (req.id)}
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3"> <div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
<div> <div>
<p class="font-semibold text-white">{req.addresseeName}</p> <p class="font-semibold text-white">{req.addresseeName}</p>
@@ -311,7 +300,7 @@
<p class="text-sm text-slate-400">Tu n'as pas encore d'amis.</p> <p class="text-sm text-slate-400">Tu n'as pas encore d'amis.</p>
{:else} {:else}
<div class="space-y-3"> <div class="space-y-3">
{#each friends as friend} {#each friends as friend (friend.id)}
<div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3"> <div class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-white/10 bg-white/5 px-4 py-3">
<div> <div>
<p class="font-semibold text-white">{friend.friendName}</p> <p class="font-semibold text-white">{friend.friendName}</p>
@@ -438,10 +427,10 @@
<p class="text-center text-slate-400">Aucun historique disponible</p> <p class="text-center text-slate-400">Aucun historique disponible</p>
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">
{#each dailyHistory as day} {#each dailyHistory as day (day.id)}
<div class="flex items-center gap-4 rounded-lg border border-white/10 bg-white/5 p-4"> <div class="flex items-center gap-4 rounded-lg border border-white/10 bg-white/5 p-4">
<!-- Character Image --> <!-- Character Image -->
<div class="flex-shrink-0"> <div class="shrink-0">
{#if day.characterImage} {#if day.characterImage}
<img <img
src={day.characterImage} src={day.characterImage}
@@ -491,7 +480,7 @@
<p class="text-center text-slate-400">Aucune session active</p> <p class="text-center text-slate-400">Aucune session active</p>
{:else} {:else}
<div class="space-y-4"> <div class="space-y-4">
{#each sessions as sess} {#each sessions as sess (sess.id)}
<div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-4 py-4"> <div class="flex items-center justify-between rounded-lg border border-white/10 bg-white/5 px-4 py-4">
<div class="flex-1"> <div class="flex-1">
<p class="font-semibold text-white"> <p class="font-semibold text-white">
@@ -537,7 +526,7 @@
<!-- Back to Home --> <!-- Back to Home -->
<div class="text-center"> <div class="text-center">
<a href="/" class="text-sm text-slate-400 transition hover:text-slate-300"> <a href={resolve("/")} class="text-sm text-slate-400 transition hover:text-slate-300">
← Retour à l'accueil ← Retour à l'accueil
</a> </a>
</div> </div>