refactor: improve type definitions and enhance state management in profile and daily components
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
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';
|
||||
|
||||
// Generate or get random seed for daily character selection
|
||||
@@ -29,14 +29,12 @@ const characterWithRelationsSelect = {
|
||||
arcName: arc.name
|
||||
};
|
||||
|
||||
export type CharacterWithRelations = typeof character.$inferSelect & {
|
||||
export type CharacterWithRelations = Character & {
|
||||
devilFruitName: string | null;
|
||||
devilFruitType: string | null;
|
||||
arcName: string | null;
|
||||
};
|
||||
|
||||
type CharacterOverrideRow = typeof characterOverride.$inferSelect;
|
||||
|
||||
type RelationMaps = {
|
||||
arcNameById: Map<string, 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(
|
||||
baseCharacter: CharacterWithRelations,
|
||||
overrideRow?: CharacterOverrideRow,
|
||||
overrideRow?: CharacterOverride,
|
||||
relationMaps?: RelationMaps
|
||||
): CharacterWithRelations {
|
||||
if (!overrideRow) {
|
||||
@@ -104,7 +102,7 @@ async function applyCharacterOverrides(
|
||||
return characters;
|
||||
}
|
||||
|
||||
const overrideByCharacterId = new Map<string, CharacterOverrideRow>(
|
||||
const overrideByCharacterId = new Map<string, CharacterOverride>(
|
||||
overrideRows.map((overrideRow) => [overrideRow.characterId, overrideRow])
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { integer, sqliteTable, text, real, unique } from 'drizzle-orm/sqlite-core';
|
||||
import { user } from './auth.schema';
|
||||
import type { InferSelectModel } from 'drizzle-orm';
|
||||
|
||||
// Define devil fruit types
|
||||
export type DevilFruitType = 'Paramecia' | 'Zoan' | 'Logia' | 'Smile' | 'Unknown';
|
||||
@@ -23,6 +24,8 @@ export const arc = sqliteTable('arc', {
|
||||
url: text('url')
|
||||
});
|
||||
|
||||
export type Arc = InferSelectModel<typeof arc>;
|
||||
|
||||
// Define the devil fruit table schema
|
||||
export const devilFruit = sqliteTable('devil_fruit', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -31,6 +34,8 @@ export const devilFruit = sqliteTable('devil_fruit', {
|
||||
url: text('url')
|
||||
});
|
||||
|
||||
export type DevilFruit = InferSelectModel<typeof devilFruit>;
|
||||
|
||||
// Define the character table schema
|
||||
export const character = sqliteTable('character', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -58,6 +63,8 @@ export const character = sqliteTable('character', {
|
||||
isInDailyMode: integer('is_in_daily_mode', { mode: 'boolean' }).default(false)
|
||||
});
|
||||
|
||||
export type Character = InferSelectModel<typeof character>;
|
||||
|
||||
// Define the character override table schema
|
||||
export const characterOverride = sqliteTable('character_override', {
|
||||
characterId: text('character_id').primaryKey().references(() => character.id, { onDelete: 'cascade' }),
|
||||
@@ -82,6 +89,8 @@ export const characterOverride = sqliteTable('character_override', {
|
||||
notes: text('notes')
|
||||
});
|
||||
|
||||
export type CharacterOverride = InferSelectModel<typeof characterOverride>;
|
||||
|
||||
// Define the character scrape validation table schema
|
||||
export const characterScrapeValidation = sqliteTable('character_scrape_validation', {
|
||||
id: text('id').primaryKey(),
|
||||
@@ -108,6 +117,8 @@ export const characterScrapeValidation = sqliteTable('character_scrape_validatio
|
||||
frUrl: text('fr_url')
|
||||
});
|
||||
|
||||
export type CharacterScrapeValidation = InferSelectModel<typeof characterScrapeValidation>;
|
||||
|
||||
// Define the character history table schema
|
||||
export const characterHistory = sqliteTable('character_history', {
|
||||
id: text('id')
|
||||
@@ -120,6 +131,8 @@ export const characterHistory = sqliteTable('character_history', {
|
||||
updatedAt: integer('updated_at').notNull().$default(() => Date.now()),
|
||||
});
|
||||
|
||||
export type CharacterHistory = InferSelectModel<typeof characterHistory>;
|
||||
|
||||
// Define the user character history table schema
|
||||
export const userCharacterHistory = sqliteTable('user_character_history', {
|
||||
id: text('id')
|
||||
@@ -134,6 +147,8 @@ export const userCharacterHistory = sqliteTable('user_character_history', {
|
||||
unique().on(table.userId, table.characterHistoryId)
|
||||
]);
|
||||
|
||||
export type UserCharacterHistory = InferSelectModel<typeof userCharacterHistory>;
|
||||
|
||||
// Define the friendship table schema (friend requests + accepted friends)
|
||||
export const friendship = sqliteTable('friendship', {
|
||||
id: text('id')
|
||||
@@ -152,5 +167,6 @@ export const friendship = sqliteTable('friendship', {
|
||||
unique().on(table.requesterId, table.addresseeId)
|
||||
]);
|
||||
|
||||
export type Friendship = InferSelectModel<typeof friendship>;
|
||||
|
||||
export * from './auth.schema';
|
||||
|
||||
@@ -1,23 +1,96 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
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';
|
||||
import type { CharacterWithRelations } from '$lib/server/daily-character.js';
|
||||
|
||||
export let data;
|
||||
|
||||
let selectedCharacters: any[] = [];
|
||||
let selectedCharacters: CharacterWithRelations[] = [];
|
||||
let isLoaded = false;
|
||||
let isGeckoMoriaWin = false;
|
||||
|
||||
let wasOriginAvailable = false;
|
||||
let wasFruitAvailable = false;
|
||||
let wasAffiliationAvailable = false;
|
||||
let showOriginUnlock = false;
|
||||
let showFruitUnlock = 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
|
||||
onMount(() => {
|
||||
@@ -37,8 +110,8 @@
|
||||
// Reconstruct character objects from IDs
|
||||
if (Array.isArray(storedIds)) {
|
||||
selectedCharacters = storedIds
|
||||
.map((id: string) => data.characters.find((c: any) => c.id === id))
|
||||
.filter((c: any) => c !== undefined);
|
||||
.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);
|
||||
@@ -50,10 +123,18 @@
|
||||
if (dailyCurrentCharacterId) {
|
||||
localStorage.setItem('dailyCurrentCharacterId', dailyCurrentCharacterId);
|
||||
}
|
||||
|
||||
syncHintAvailability(0, selectedCharacters.length);
|
||||
|
||||
isLoaded = true;
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
clearUnlockTimeout(originUnlockTimeout);
|
||||
clearUnlockTimeout(fruitUnlockTimeout);
|
||||
clearUnlockTimeout(affiliationUnlockTimeout);
|
||||
});
|
||||
|
||||
// Save to localStorage whenever selectedCharacters changes (only store IDs)
|
||||
$: if (isLoaded && selectedCharacters) {
|
||||
const ids = selectedCharacters.map(char => char.id);
|
||||
@@ -66,39 +147,15 @@
|
||||
$: columnVisibility = data.columnVisibility || {};
|
||||
$: 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) {
|
||||
const character = event.detail;
|
||||
selectCharacter(character);
|
||||
}
|
||||
|
||||
function selectCharacter(character: any) {
|
||||
function selectCharacter(character: CharacterWithRelations) {
|
||||
const previousGuessCount = selectedCharacters.length;
|
||||
selectedCharacters = [character, ...selectedCharacters];
|
||||
syncHintAvailability(previousGuessCount, selectedCharacters.length, isLoaded);
|
||||
|
||||
// Check if player won
|
||||
if (character.id === dailyCharacter.id) {
|
||||
@@ -122,7 +179,9 @@
|
||||
}
|
||||
|
||||
function resetHistory() {
|
||||
const previousGuessCount = selectedCharacters.length;
|
||||
selectedCharacters = [];
|
||||
syncHintAvailability(previousGuessCount, 0);
|
||||
localStorage.removeItem('dailyCharacterHistory');
|
||||
}
|
||||
</script>
|
||||
@@ -198,7 +257,7 @@
|
||||
<main
|
||||
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="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">
|
||||
<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">
|
||||
{#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 gap-3">
|
||||
{#if friendResult.image}
|
||||
|
||||
@@ -190,6 +190,7 @@ export const actions: Actions = {
|
||||
// Delete the session from database
|
||||
await db.delete(session).where(eq(session.id, sessionId));
|
||||
} catch (error) {
|
||||
console.error('Error revoking session:', error);
|
||||
return fail(500, { message: 'Erreur lors de la révocation de la session' });
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageData } from './$types';
|
||||
import { resolve } from '$app/paths';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
@@ -11,41 +12,29 @@
|
||||
|
||||
let isLoading = $state(false);
|
||||
let activeTab = $state<'profile' | 'password' | 'sessions' | 'daily' | 'friends'>('profile');
|
||||
let name = $state('');
|
||||
let name = $derived(data.user?.name || '');
|
||||
let friendUsername = $state('');
|
||||
let showSuccess = $state(false);
|
||||
let oldPassword = $state('');
|
||||
let newPassword = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let sessions = $state<any[]>([]);
|
||||
let dailyHistory = $state<any[]>([]);
|
||||
let friends = $state<any[]>([]);
|
||||
let incomingRequests = $state<any[]>([]);
|
||||
let outgoingRequests = $state<any[]>([]);
|
||||
let sessions = $derived(data.sessions || []);
|
||||
let dailyHistory = $derived(data.dailyHistory || []);
|
||||
let friends = $derived(data.friends || []);
|
||||
let incomingRequests = $derived(data.incomingRequests || []);
|
||||
let outgoingRequests = $derived(data.outgoingRequests || []);
|
||||
let tabsElement: HTMLDivElement | undefined;
|
||||
|
||||
$effect(() => {
|
||||
name = data.user?.name || '';
|
||||
friends = data.friends || [];
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
sessions = (data as any).sessions || [];
|
||||
incomingRequests = data.incomingRequests || [];
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
dailyHistory = (data as any).dailyHistory || [];
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
friends = (data as any).friends || [];
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
incomingRequests = (data as any).incomingRequests || [];
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
outgoingRequests = (data as any).outgoingRequests || [];
|
||||
outgoingRequests = data.outgoingRequests || [];
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
@@ -71,7 +60,7 @@
|
||||
</svelte:head>
|
||||
|
||||
<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="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">
|
||||
<button
|
||||
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'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
@@ -98,7 +87,7 @@
|
||||
</button>
|
||||
<button
|
||||
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'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
@@ -106,7 +95,7 @@
|
||||
</button>
|
||||
<button
|
||||
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'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
@@ -114,7 +103,7 @@
|
||||
</button>
|
||||
<button
|
||||
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'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
@@ -122,7 +111,7 @@
|
||||
</button>
|
||||
<button
|
||||
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'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
@@ -261,7 +250,7 @@
|
||||
<p class="text-sm text-slate-400">Aucune demande reçue.</p>
|
||||
{:else}
|
||||
<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>
|
||||
<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>
|
||||
{:else}
|
||||
<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>
|
||||
<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>
|
||||
{:else}
|
||||
<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>
|
||||
<p class="font-semibold text-white">{friend.friendName}</p>
|
||||
@@ -438,10 +427,10 @@
|
||||
<p class="text-center text-slate-400">Aucun historique disponible</p>
|
||||
{:else}
|
||||
<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">
|
||||
<!-- Character Image -->
|
||||
<div class="flex-shrink-0">
|
||||
<div class="shrink-0">
|
||||
{#if day.characterImage}
|
||||
<img
|
||||
src={day.characterImage}
|
||||
@@ -491,7 +480,7 @@
|
||||
<p class="text-center text-slate-400">Aucune session active</p>
|
||||
{:else}
|
||||
<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-1">
|
||||
<p class="font-semibold text-white">
|
||||
@@ -537,7 +526,7 @@
|
||||
|
||||
<!-- Back to Home -->
|
||||
<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
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user