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:
2026-03-01 23:01:44 +01:00
parent e45dfb9832
commit 114f6cde7a
28 changed files with 2603 additions and 31 deletions

View File

@@ -0,0 +1,266 @@
<script lang="ts">
import { enhance } from '$app/forms';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
interface ConfigItem {
key: string;
value: string;
}
let { data }: Props = $props();
let configItems = $state<ConfigItem[]>([]);
let newKey = $state('');
let newValue = $state('');
let editingKey = $state<string | null>(null);
let editingValue = $state('');
let isSaving = $state(false);
let saveMessage = $state<{ type: 'success' | 'error'; text: string } | null>(null);
$effect(() => {
configItems = data.config.map((item) => ({
key: item.key,
value: item.value ?? ''
}));
});
const startEdit = (item: ConfigItem) => {
editingKey = item.key;
editingValue = item.value;
};
const cancelEdit = () => {
editingKey = null;
editingValue = '';
saveMessage = null;
};
const handleAddNew = async () => {
if (!newKey || !newValue) {
saveMessage = { type: 'error', text: 'Both key and value are required' };
return;
}
if (configItems.some((item) => item.key === newKey)) {
saveMessage = { type: 'error', text: 'A config with this key already exists' };
return;
}
isSaving = true;
const formData = new FormData();
formData.append('key', newKey);
formData.append('value', newValue);
try {
const response = await fetch('?/update', {
method: 'POST',
body: formData
});
if (response.ok) {
configItems = [...configItems, { key: newKey, value: newValue }];
newKey = '';
newValue = '';
saveMessage = { type: 'success', text: 'Config added successfully' };
} else {
saveMessage = { type: 'error', text: 'Failed to add config' };
}
} catch (error) {
saveMessage = { type: 'error', text: 'Error adding config' };
} finally {
isSaving = false;
setTimeout(() => {
saveMessage = null;
}, 3000);
}
};
const handleDelete = async (key: string) => {
if (!confirm(`Are you sure you want to delete "${key}"?`)) return;
isSaving = true;
const formData = new FormData();
formData.append('key', key);
try {
const response = await fetch('?/delete', {
method: 'POST',
body: formData
});
if (response.ok) {
configItems = configItems.filter((item) => item.key !== key);
saveMessage = { type: 'success', text: 'Config deleted successfully' };
} else {
saveMessage = { type: 'error', text: 'Failed to delete config' };
}
} catch (error) {
saveMessage = { type: 'error', text: 'Error deleting config' };
} finally {
isSaving = false;
setTimeout(() => {
saveMessage = null;
}, 3000);
}
};
</script>
<svelte:head>
<title>Settings - Admin - OnePieceDle</title>
</svelte:head>
<div class="space-y-6">
<!-- Header -->
<h2 class="text-3xl font-bold text-white">Configuration</h2>
<!-- Add New Config -->
<div class="rounded-lg border border-white/10 bg-slate-800/50 p-6">
<h3 class="mb-4 text-lg font-semibold text-white">Add New Configuration</h3>
<div class="flex gap-4">
<input
type="text"
placeholder="Key name"
bind:value={newKey}
class="flex-1 rounded-lg bg-slate-700 px-4 py-2 text-sm text-white placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-amber-600"
/>
<input
type="text"
placeholder="Value"
bind:value={newValue}
class="flex-1 rounded-lg bg-slate-700 px-4 py-2 text-sm text-white placeholder-gray-400 outline-none transition focus:ring-2 focus:ring-amber-600"
/>
<button
onclick={handleAddNew}
disabled={isSaving}
class="rounded-lg bg-amber-600 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-amber-700 disabled:opacity-50"
>
Add
</button>
</div>
</div>
<!-- Config Table -->
<div class="rounded-lg border border-white/10">
<div class="max-h-[calc(100vh-20rem)] overflow-auto">
<table class="w-full">
<thead class="sticky top-0 bg-slate-800 z-10">
<tr class="border-b border-white/10">
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-300">Key</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-300">Value</th>
<th class="px-6 py-3 text-left text-sm font-semibold text-gray-300">Actions</th>
</tr>
</thead>
<tbody>
{#each configItems as item}
{#if editingKey === item.key}
<tr class="border-b border-white/5 bg-slate-800/50">
<td class="px-6 py-4 text-sm text-white">{item.key}</td>
<td class="px-6 py-4 text-sm">
<input
type="text"
bind:value={editingValue}
class="w-full rounded-lg bg-slate-700 px-3 py-1 text-sm text-white outline-none transition focus:ring-2 focus:ring-amber-600"
/>
</td>
<td class="px-6 py-4 text-sm">
<form
method="POST"
action="?/update"
use:enhance={() => {
isSaving = true;
return async ({ result }) => {
isSaving = false;
if (result.type === 'success') {
const idx = configItems.findIndex((i) => i.key === item.key);
if (idx !== -1) {
configItems[idx].value = editingValue;
}
editingKey = null;
saveMessage = { type: 'success', text: 'Config updated' };
} else if (result.type === 'failure') {
saveMessage = { type: 'error', text: (result.data?.error as string) || 'Failed to update' };
} else {
saveMessage = { type: 'error', text: 'Failed to update' };
}
setTimeout(() => {
saveMessage = null;
}, 3000);
};
}}
>
<input type="hidden" name="key" value={item.key} />
<input type="hidden" name="value" value={editingValue} />
<div class="flex gap-2">
<button
type="submit"
disabled={isSaving}
class="rounded bg-green-600 px-3 py-1 text-xs font-medium text-white hover:bg-green-700 disabled:opacity-50"
>
Save
</button>
<button
type="button"
onclick={cancelEdit}
disabled={isSaving}
class="rounded bg-gray-600 px-3 py-1 text-xs font-medium text-white hover:bg-gray-700 disabled:opacity-50"
>
Cancel
</button>
</div>
</form>
</td>
</tr>
{:else}
<tr class="border-b border-white/5 hover:bg-slate-800/50">
<td class="px-6 py-4 text-sm font-medium text-white">{item.key}</td>
<td class="px-6 py-4 text-sm text-gray-400">
<code class="rounded bg-slate-800/50 px-2 py-1">{item.value}</code>
</td>
<td class="px-6 py-4 text-sm">
<div class="flex items-center gap-2">
<button
onclick={() => startEdit(item)}
class="text-amber-400 hover:text-amber-300 transition-colors"
title="Edit config"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
</button>
<button
onclick={() => handleDelete(item.key)}
disabled={isSaving}
class="text-red-400 hover:text-red-300 transition-colors disabled:opacity-50"
title="Delete config"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</div>
</td>
</tr>
{/if}
{/each}
</tbody>
</table> </div> </div>
{#if configItems.length === 0}
<div class="rounded-lg border border-white/10 bg-slate-800/50 py-12 text-center">
<p class="text-gray-400">No configuration entries yet</p>
</div>
{/if}
<!-- Save Message -->
{#if saveMessage}
<div
class={`rounded-lg p-4 text-sm font-medium ${
saveMessage.type === 'success'
? 'border border-green-500/50 bg-green-500/10 text-green-300'
: 'border border-red-500/50 bg-red-500/10 text-red-300'
}`}
>
{saveMessage.text}
</div>
{/if}
</div>