up
This commit is contained in:
157
frontend/src/routes/users/+page.svelte
Normal file
157
frontend/src/routes/users/+page.svelte
Normal file
@@ -0,0 +1,157 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { api } from '$lib/stores/api.js';
|
||||
import { Users, Plus, Trash2, X, Check, AlertCircle } from 'lucide-svelte';
|
||||
|
||||
let users = [];
|
||||
let loading = true;
|
||||
let showForm = false;
|
||||
let error = '';
|
||||
let successMsg = '';
|
||||
|
||||
let form = { email: '', name: '', password: '', confirm: '' };
|
||||
|
||||
onMount(load);
|
||||
|
||||
async function load() {
|
||||
loading = true;
|
||||
users = await api.users.list() || [];
|
||||
loading = false;
|
||||
}
|
||||
|
||||
function openForm() {
|
||||
form = { email: '', name: '', password: '', confirm: '' };
|
||||
error = '';
|
||||
showForm = true;
|
||||
}
|
||||
|
||||
async function create() {
|
||||
error = '';
|
||||
if (!form.email || !form.name || !form.password) { error = 'Tous les champs sont requis.'; return; }
|
||||
if (form.password !== form.confirm) { error = 'Les mots de passe ne correspondent pas.'; return; }
|
||||
if (form.password.length < 6) { error = 'Minimum 6 caractères.'; return; }
|
||||
try {
|
||||
await api.auth.register({ email: form.email, name: form.name, password: form.password });
|
||||
showForm = false;
|
||||
successMsg = `Compte "${form.name}" créé avec succès.`;
|
||||
setTimeout(() => successMsg = '', 4000);
|
||||
await load();
|
||||
} catch (e) {
|
||||
error = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async function remove(id, name) {
|
||||
if (!confirm(`Supprimer le compte de "${name}" ?`)) return;
|
||||
try {
|
||||
await api.users.delete(id);
|
||||
await load();
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const fmtDate = (d) => new Date(d).toLocaleDateString('fr-FR', { day: '2-digit', month: 'long', year: 'numeric' });
|
||||
</script>
|
||||
|
||||
<div class="p-6 max-w-2xl mx-auto">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<Users size={22} class="text-gray-400"/>
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-white">Utilisateurs</h1>
|
||||
</div>
|
||||
<button on:click={openForm}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
<Plus size={16}/> Ajouter un membre
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-sm text-gray-500 dark:text-gray-400 mb-5">
|
||||
Ajoutez les membres de votre famille qui peuvent accéder à l'application.
|
||||
Chacun a son propre compte et mot de passe.
|
||||
</p>
|
||||
|
||||
{#if successMsg}
|
||||
<div class="flex items-center gap-2 text-sm px-4 py-3 rounded-lg mb-4 bg-green-50 dark:bg-green-950/30 text-green-700 dark:text-green-300">
|
||||
<Check size={14}/> {successMsg}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if loading}
|
||||
<div class="space-y-2">
|
||||
{#each [1,2] as _}<div class="h-16 bg-gray-100 dark:bg-gray-800 rounded-xl animate-pulse"/>{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="bg-white dark:bg-gray-900 rounded-xl border border-gray-100 dark:border-gray-800 overflow-hidden">
|
||||
{#each users as u, i (u.id)}
|
||||
<div class="flex items-center gap-4 px-5 py-4
|
||||
{i > 0 ? 'border-t border-gray-50 dark:border-gray-800' : ''}">
|
||||
<!-- Avatar -->
|
||||
<div class="w-9 h-9 rounded-full bg-blue-100 dark:bg-blue-900 flex items-center justify-center text-sm font-semibold text-blue-700 dark:text-blue-300 shrink-0">
|
||||
{u.name?.[0]?.toUpperCase() ?? '?'}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">{u.name}</p>
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500">{u.email} · Depuis le {fmtDate(u.created_at)}</p>
|
||||
</div>
|
||||
{#if i === 0}
|
||||
<span class="text-xs px-2 py-0.5 rounded-full bg-blue-50 dark:bg-blue-950 text-blue-700 dark:text-blue-300 font-medium shrink-0">
|
||||
Admin
|
||||
</span>
|
||||
{:else}
|
||||
<button on:click={() => remove(u.id, u.name)}
|
||||
class="p-2 text-gray-300 hover:text-red-500 rounded-lg hover:bg-red-50 dark:hover:bg-red-950 transition-colors shrink-0">
|
||||
<Trash2 size={15}/>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Modal -->
|
||||
{#if showForm}
|
||||
<div class="fixed inset-0 bg-black/40 flex items-center justify-center z-50 p-4" on:click|self={() => showForm = false}>
|
||||
<div class="bg-white dark:bg-gray-900 rounded-2xl w-full max-w-md shadow-xl border border-gray-100 dark:border-gray-800">
|
||||
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100 dark:border-gray-800">
|
||||
<h2 class="font-semibold text-gray-900 dark:text-white">Ajouter un membre</h2>
|
||||
<button on:click={() => showForm = false} class="text-gray-400 hover:text-gray-600"><X size={18}/></button>
|
||||
</div>
|
||||
<div class="px-6 py-5 space-y-4">
|
||||
{#if error}
|
||||
<div class="flex items-center gap-2 text-sm px-3 py-2 rounded-lg bg-red-50 dark:bg-red-950/30 text-red-600 dark:text-red-400">
|
||||
<AlertCircle size={13}/> {error}
|
||||
</div>
|
||||
{/if}
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Nom affiché</label>
|
||||
<input bind:value={form.name} placeholder="Ex: Marie Dupont"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Email</label>
|
||||
<input type="email" bind:value={form.email} placeholder="marie@exemple.fr"
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Mot de passe</label>
|
||||
<input type="password" bind:value={form.password}
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">Confirmer le mot de passe</label>
|
||||
<input type="password" bind:value={form.confirm}
|
||||
class="w-full px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3 px-6 py-4 border-t border-gray-100 dark:border-gray-800">
|
||||
<button on:click={() => showForm = false} class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400">Annuler</button>
|
||||
<button on:click={create}
|
||||
class="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium transition-colors">
|
||||
<Check size={15}/> Créer le compte
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
Reference in New Issue
Block a user