Initial commit
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
---
|
||||
import BaseLayout from "../layouts/BaseLayout.astro";
|
||||
|
||||
const { pb } = Astro.locals;
|
||||
|
||||
if (!pb.authStore.isValid) {
|
||||
return Astro.redirect("/login");
|
||||
}
|
||||
|
||||
const user = pb.authStore.record!;
|
||||
const initials = (user.name || user.email || "")
|
||||
.split(/[\s@.]+/)
|
||||
.filter(Boolean)
|
||||
.slice(0, 2)
|
||||
.map((s: string) => s[0].toUpperCase())
|
||||
.join("");
|
||||
|
||||
const themePreference = user.theme_preference ?? "System";
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<div class="max-w-2xl mx-auto px-4 md:px-8 py-8">
|
||||
<h1 class="text-xl font-bold mb-1">Profil</h1>
|
||||
<p class="text-sm opacity-70 mb-8">
|
||||
Gérez votre compte et vos préférences.
|
||||
</p>
|
||||
|
||||
<div class="tabs" id="profile-tabs">
|
||||
<nav role="tablist" aria-orientation="horizontal" class="mb-6">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="tab-account"
|
||||
aria-controls="panel-account"
|
||||
aria-selected="true"
|
||||
tabindex="0"
|
||||
>
|
||||
Compte
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
id="tab-preferences"
|
||||
aria-controls="panel-preferences"
|
||||
aria-selected="false"
|
||||
tabindex="-1"
|
||||
>
|
||||
Préférences
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- Account Panel -->
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="panel-account"
|
||||
aria-labelledby="tab-account"
|
||||
tabindex="-1"
|
||||
aria-selected="true"
|
||||
>
|
||||
<div class="card">
|
||||
<header>
|
||||
<h2>Compte</h2>
|
||||
</header>
|
||||
<section>
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<span class="avatar" data-size="lg">
|
||||
<span>{initials}</span>
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-semibold"
|
||||
>{user.name || "—"}</span
|
||||
>
|
||||
<span class="text-sm opacity-70"
|
||||
>{user.email}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="account-form" class="grid gap-4">
|
||||
<div role="group" class="field">
|
||||
<label for="username">Nom d'utilisateur</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={user.username ?? ""}
|
||||
placeholder="ex: pikalover"
|
||||
minlength="3"
|
||||
maxlength="20"
|
||||
/>
|
||||
<p>
|
||||
Votre @username unique (3-20 caractères,
|
||||
lettres minuscules et chiffres).
|
||||
</p>
|
||||
</div>
|
||||
<div role="group" class="field">
|
||||
<label for="name">Nom complet</label>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={user.name ?? ""}
|
||||
placeholder="Votre nom complet"
|
||||
/>
|
||||
<p>Votre nom affiché dans l'application.</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer>
|
||||
<button
|
||||
class="btn"
|
||||
type="submit"
|
||||
form="account-form"
|
||||
id="account-submit"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preferences Panel -->
|
||||
<div
|
||||
role="tabpanel"
|
||||
id="panel-preferences"
|
||||
aria-labelledby="tab-preferences"
|
||||
tabindex="-1"
|
||||
aria-selected="false"
|
||||
hidden
|
||||
>
|
||||
<div class="card">
|
||||
<header>
|
||||
<h2>Préférences</h2>
|
||||
<p>Personnalisez votre expérience.</p>
|
||||
</header>
|
||||
<section>
|
||||
<form id="profile-form" class="grid gap-4">
|
||||
<div role="group" class="field">
|
||||
<label for="theme_preference">Thème</label>
|
||||
<select
|
||||
name="theme_preference"
|
||||
id="theme_preference"
|
||||
class="select w-full"
|
||||
>
|
||||
<option
|
||||
value="System"
|
||||
selected={themePreference === "System"}
|
||||
>
|
||||
Système
|
||||
</option>
|
||||
<option
|
||||
value="Light"
|
||||
selected={themePreference === "Light"}
|
||||
>
|
||||
Clair
|
||||
</option>
|
||||
<option
|
||||
value="Dark"
|
||||
selected={themePreference === "Dark"}
|
||||
>
|
||||
Sombre
|
||||
</option>
|
||||
</select>
|
||||
<p>Choisissez le thème de l'interface.</p>
|
||||
</div>
|
||||
|
||||
<div role="group" class="field">
|
||||
<label for="visibility"
|
||||
>Visibilité de la collection</label
|
||||
>
|
||||
<select
|
||||
name="visibility"
|
||||
id="visibility"
|
||||
class="select w-full"
|
||||
>
|
||||
<option
|
||||
value="Friends"
|
||||
selected={user.visibility !==
|
||||
"Public" &&
|
||||
user.visibility !== "Private"}
|
||||
>
|
||||
Amis seulement
|
||||
</option>
|
||||
<option
|
||||
value="Public"
|
||||
selected={user.visibility === "Public"}
|
||||
>
|
||||
Publique
|
||||
</option>
|
||||
<option
|
||||
value="Private"
|
||||
selected={user.visibility === "Private"}
|
||||
>
|
||||
Privée
|
||||
</option>
|
||||
</select>
|
||||
<p>Qui peut voir votre collection.</p>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<footer>
|
||||
<button
|
||||
class="btn"
|
||||
type="submit"
|
||||
form="profile-form"
|
||||
id="profile-submit"
|
||||
>
|
||||
Enregistrer
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
const toaster = document.getElementById("toaster") as ToasterElement;
|
||||
|
||||
const showToast = (
|
||||
category: "success" | "error",
|
||||
title: string,
|
||||
description: string,
|
||||
) => {
|
||||
if (toaster?.toast) {
|
||||
toaster.toast({ category, title, description });
|
||||
}
|
||||
};
|
||||
|
||||
// Account form (name)
|
||||
const accountForm = document.getElementById("account-form");
|
||||
const accountBtn = document.getElementById(
|
||||
"account-submit",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
accountForm?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(accountForm as HTMLFormElement);
|
||||
const name = formData.get("name");
|
||||
const username = formData.get("username");
|
||||
|
||||
accountBtn.disabled = true;
|
||||
const originalText = accountBtn.innerHTML;
|
||||
accountBtn.innerHTML = `<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg> Enregistrement...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/profile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name, username }),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showToast(
|
||||
"success",
|
||||
"Enregistré",
|
||||
"Votre nom a été mis à jour.",
|
||||
);
|
||||
// Reload to reflect the new name in the nav
|
||||
setTimeout(() => window.location.reload(), 1000);
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast(
|
||||
"error",
|
||||
"Erreur",
|
||||
data.error ?? "Échec de l'enregistrement",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
} finally {
|
||||
accountBtn.disabled = false;
|
||||
accountBtn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
|
||||
// Preferences form (theme)
|
||||
const profileForm = document.getElementById("profile-form");
|
||||
const profileBtn = document.getElementById(
|
||||
"profile-submit",
|
||||
) as HTMLButtonElement;
|
||||
|
||||
profileForm?.addEventListener("submit", async (e) => {
|
||||
e.preventDefault();
|
||||
const formData = new FormData(profileForm as HTMLFormElement);
|
||||
const themePreference = formData.get("theme_preference");
|
||||
const visibility = formData.get("visibility");
|
||||
|
||||
profileBtn.disabled = true;
|
||||
const originalText = profileBtn.innerHTML;
|
||||
profileBtn.innerHTML = `<svg aria-label="Loading" role="status" data-icon="inline-start" class="animate-spin lucide lucide-loader-circle" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg> Enregistrement...`;
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/profile", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
theme_preference: themePreference,
|
||||
visibility,
|
||||
}),
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
showToast(
|
||||
"success",
|
||||
"Enregistré",
|
||||
"Vos préférences ont été mises à jour.",
|
||||
);
|
||||
// Apply theme instantly without refresh
|
||||
const html = document.documentElement;
|
||||
if (themePreference === "Dark") {
|
||||
html.classList.add("dark");
|
||||
html.removeAttribute("data-theme");
|
||||
} else if (themePreference === "Light") {
|
||||
html.classList.remove("dark");
|
||||
html.setAttribute("data-theme", "light");
|
||||
} else {
|
||||
// System — use system preference
|
||||
html.classList.toggle(
|
||||
"dark",
|
||||
matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
html.removeAttribute("data-theme");
|
||||
}
|
||||
} else {
|
||||
const data = await res.json();
|
||||
showToast(
|
||||
"error",
|
||||
"Erreur",
|
||||
data.error ?? "Échec de l'enregistrement",
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
showToast("error", "Erreur", "Erreur réseau");
|
||||
} finally {
|
||||
profileBtn.disabled = false;
|
||||
profileBtn.innerHTML = originalText;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user