feat: add daily win recording endpoint, user authentication, and profile management
- Implemented a POST endpoint for recording daily wins in the game. - Created login and signup functionality with email and password. - Developed a profile page allowing users to update their profile information, change passwords, and manage active sessions. - Added a toggle feature for switching between login and signup forms. - Enhanced the layout by removing the profile button and adjusting the header structure.
This commit is contained in:
334
src/routes/(game)/profile/+page.svelte
Normal file
334
src/routes/(game)/profile/+page.svelte
Normal file
@@ -0,0 +1,334 @@
|
||||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
form?: { success?: boolean; message?: string } | null;
|
||||
}
|
||||
|
||||
let { data, form }: Props = $props();
|
||||
|
||||
let isLoading = $state(false);
|
||||
let activeTab = $state<'profile' | 'password' | 'sessions'>('profile');
|
||||
let name = $state('');
|
||||
let showSuccess = $state(false);
|
||||
let oldPassword = $state('');
|
||||
let newPassword = $state('');
|
||||
let confirmPassword = $state('');
|
||||
let sessions = $state<any[]>([]);
|
||||
let tabsElement: HTMLDivElement | undefined;
|
||||
|
||||
$effect(() => {
|
||||
name = data.user?.name || '';
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
sessions = (data as any).sessions || [];
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
if (form && form.success === true) {
|
||||
showSuccess = true;
|
||||
setTimeout(() => {
|
||||
showSuccess = false;
|
||||
}, 3000);
|
||||
}
|
||||
});
|
||||
|
||||
const handleTabChange = (tab: 'profile' | 'password' | 'sessions') => {
|
||||
activeTab = tab;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
// Just for type purposes
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Mon Profil - OnePieceDle</title>
|
||||
</svelte:head>
|
||||
|
||||
<main class="relative min-h-screen overflow-hidden 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 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-2xl flex-col items-center px-6 py-4">
|
||||
<div class="w-full space-y-4">
|
||||
<!-- Header -->
|
||||
<div class="text-center">
|
||||
<h1 class="text-3xl font-black uppercase tracking-[0.3em] text-amber-50 sm:text-4xl">
|
||||
Mon Profil
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-slate-300">
|
||||
Modifie les informations de ton profil
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Tabs Navigation -->
|
||||
<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'
|
||||
? 'border-b-2 border-amber-300 text-amber-100'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
Profil
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleTabChange('password')}
|
||||
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'password'
|
||||
? 'border-b-2 border-amber-300 text-amber-100'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
Mot de passe
|
||||
</button>
|
||||
<button
|
||||
onclick={() => handleTabChange('sessions')}
|
||||
class="px-4 py-3 font-semibold uppercase tracking-[0.1em] transition {activeTab === 'sessions'
|
||||
? 'border-b-2 border-amber-300 text-amber-100'
|
||||
: 'text-slate-400 hover:text-slate-100'}"
|
||||
>
|
||||
Sessions
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tab Content -->
|
||||
{#if activeTab === 'profile'}
|
||||
<div class="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">
|
||||
<!-- Avatar -->
|
||||
<div class="mb-6 flex flex-col items-center gap-4">
|
||||
{#if data.user.image}
|
||||
<img
|
||||
src={data.user.image}
|
||||
alt={data.user.name || 'Profil'}
|
||||
class="h-24 w-24 rounded-full border-2 border-amber-300 object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div class="flex h-24 w-24 items-center justify-center rounded-full border-2 border-amber-300 bg-amber-300/20 text-2xl font-semibold text-amber-100">
|
||||
{data.user.name?.charAt(0).toUpperCase() || 'U'}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="text-center">
|
||||
<p class="text-sm text-slate-400">Email</p>
|
||||
<p class="font-semibold text-white">{data.user.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form -->
|
||||
<form
|
||||
method="POST"
|
||||
action="?/updateProfile"
|
||||
use:enhance={() => {
|
||||
isLoading = true;
|
||||
return async ({ update }) => {
|
||||
isLoading = false;
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
onsubmit={handleSubmit}
|
||||
class="space-y-6"
|
||||
>
|
||||
<!-- Name Field -->
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Nom d'affichage
|
||||
</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
name="name"
|
||||
bind:value={name}
|
||||
required
|
||||
placeholder="Ton nom"
|
||||
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if form && form.message && form.success !== true}
|
||||
<div class="rounded-lg border border-red-500/30 bg-red-900/20 px-4 py-3 text-sm text-red-200">
|
||||
{form.message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Success Message -->
|
||||
{#if showSuccess}
|
||||
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
|
||||
Profil mis à jour avec succès !
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
|
||||
>
|
||||
{isLoading ? 'Mise à jour...' : 'Enregistrer les modifications'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Password Tab -->
|
||||
{#if activeTab === 'password'}
|
||||
<div class="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="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
|
||||
Changer le mot de passe
|
||||
</h2>
|
||||
|
||||
<!-- Form -->
|
||||
<form
|
||||
method="POST"
|
||||
action="?/changePassword"
|
||||
use:enhance={() => {
|
||||
isLoading = true;
|
||||
return async ({ update }) => {
|
||||
isLoading = false;
|
||||
oldPassword = '';
|
||||
newPassword = '';
|
||||
confirmPassword = '';
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
class="space-y-6"
|
||||
>
|
||||
<!-- Old Password Field -->
|
||||
<div>
|
||||
<label for="oldPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Mot de passe actuel
|
||||
</label>
|
||||
<input
|
||||
id="oldPassword"
|
||||
type="password"
|
||||
name="oldPassword"
|
||||
bind:value={oldPassword}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- New Password Field -->
|
||||
<div>
|
||||
<label for="newPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Nouveau mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="newPassword"
|
||||
type="password"
|
||||
name="newPassword"
|
||||
bind:value={newPassword}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Password Field -->
|
||||
<div>
|
||||
<label for="confirmPassword" class="block text-sm font-semibold uppercase tracking-[0.2em] text-amber-100">
|
||||
Confirmer le mot de passe
|
||||
</label>
|
||||
<input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
bind:value={confirmPassword}
|
||||
required
|
||||
placeholder="••••••••"
|
||||
class="mt-3 w-full rounded-lg border border-white/10 bg-white/5 px-4 py-3 text-white placeholder-slate-500 transition focus:border-amber-300 focus:outline-none focus:ring-2 focus:ring-amber-300/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Error Message -->
|
||||
{#if form && form.message && form.success !== true}
|
||||
<div class="rounded-lg border border-red-500/30 bg-red-900/20 px-4 py-3 text-sm text-red-200">
|
||||
{form.message}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Success Message -->
|
||||
{#if showSuccess}
|
||||
<div class="rounded-lg border border-green-500/30 bg-green-900/20 px-4 py-3 text-sm text-green-200">
|
||||
Mot de passe changé avec succès !
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Submit Button -->
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
class="w-full rounded-full bg-amber-300 px-6 py-3 text-sm font-semibold text-slate-900 transition disabled:opacity-50 hover:bg-amber-200"
|
||||
>
|
||||
{isLoading ? 'Changement en cours...' : 'Changer le mot de passe'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Sessions Tab -->
|
||||
{#if activeTab === 'sessions'}
|
||||
<div class="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="mb-6 text-2xl font-bold uppercase tracking-[0.2em] text-amber-50">
|
||||
Sessions actives
|
||||
</h2>
|
||||
|
||||
{#if sessions.length === 0}
|
||||
<p class="text-center text-slate-400">Aucune session active</p>
|
||||
{:else}
|
||||
<div class="space-y-4">
|
||||
{#each sessions as sess}
|
||||
<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">
|
||||
{sess.userAgent || 'Appareil inconnu'}
|
||||
</p>
|
||||
<p class="text-xs text-slate-400">
|
||||
IP: {sess.ipAddress || 'Inconnue'}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-slate-500">
|
||||
Créée: {new Date(sess.createdAt).toLocaleDateString('fr-FR', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/revokeSession"
|
||||
style="display: inline;"
|
||||
use:enhance={() => {
|
||||
return async ({ update }) => {
|
||||
await update();
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="sessionId" value={sess.id} />
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-lg border border-red-500/50 bg-red-900/20 px-4 py-2 text-xs font-semibold text-red-300 transition hover:border-red-500 hover:bg-red-900/40"
|
||||
>
|
||||
Terminer
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Back to Home -->
|
||||
<div class="text-center">
|
||||
<a href="/" class="text-sm text-slate-400 transition hover:text-slate-300">
|
||||
← Retour à l'accueil
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
Reference in New Issue
Block a user