Refactor component imports and structure: Update import paths for various components to improve organization, moving them into appropriate subdirectories. Remove unused components related to user and event management, enhancing code clarity and maintainability across the application.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m36s
This commit is contained in:
222
components/profile/PlayerStats.tsx
Normal file
222
components/profile/PlayerStats.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { Avatar } from "@/components/ui";
|
||||
|
||||
interface UserData {
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
xp: number;
|
||||
maxXp: number;
|
||||
level: number;
|
||||
}
|
||||
|
||||
interface PlayerStatsProps {
|
||||
initialUserData?: UserData | null;
|
||||
}
|
||||
|
||||
// Format number with consistent locale to avoid hydration mismatch
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
const defaultUserData: UserData = {
|
||||
username: "Guest",
|
||||
avatar: null,
|
||||
hp: 1000,
|
||||
maxHp: 1000,
|
||||
xp: 0,
|
||||
maxXp: 5000,
|
||||
level: 1,
|
||||
};
|
||||
|
||||
export default function PlayerStats({ initialUserData }: PlayerStatsProps) {
|
||||
const { data: session } = useSession();
|
||||
const [userData, setUserData] = useState<UserData>(
|
||||
initialUserData || defaultUserData
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Si on a déjà des données initiales, ne rien faire (déjà initialisé dans useState)
|
||||
if (initialUserData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Sinon, fallback sur le fetch côté client (pour les pages Client Components)
|
||||
if (session?.user?.id) {
|
||||
fetch(`/api/users/${session.user.id}`)
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
if (data) {
|
||||
// Utiliser requestAnimationFrame pour éviter les cascades de rendu
|
||||
requestAnimationFrame(() => {
|
||||
setUserData({
|
||||
username: data.username || "Guest",
|
||||
avatar: data.avatar,
|
||||
hp: data.hp || 1000,
|
||||
maxHp: data.maxHp || 1000,
|
||||
xp: data.xp || 0,
|
||||
maxXp: data.maxXp || 5000,
|
||||
level: data.level || 1,
|
||||
});
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Utiliser les données de session si l'API échoue
|
||||
requestAnimationFrame(() => {
|
||||
setUserData({
|
||||
username: session.user.username || "Guest",
|
||||
avatar: null,
|
||||
hp: 1000,
|
||||
maxHp: 1000,
|
||||
xp: 0,
|
||||
maxXp: 5000,
|
||||
level: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
} else if (!initialUserData) {
|
||||
// Utiliser requestAnimationFrame pour éviter les cascades de rendu
|
||||
requestAnimationFrame(() => {
|
||||
setUserData(defaultUserData);
|
||||
});
|
||||
}
|
||||
}, [session, initialUserData]);
|
||||
|
||||
const { username, avatar, hp, maxHp, xp, maxXp, level } = userData;
|
||||
|
||||
// Calculer les pourcentages cibles
|
||||
const targetHpPercentage = (hp / maxHp) * 100;
|
||||
const targetXpPercentage = (xp / maxXp) * 100;
|
||||
|
||||
// Initialiser les pourcentages à 0 si on a des données initiales (pour l'animation)
|
||||
// Sinon utiliser directement les valeurs calculées
|
||||
const [hpPercentage, setHpPercentage] = useState(
|
||||
initialUserData ? 0 : targetHpPercentage
|
||||
);
|
||||
const [xpPercentage, setXpPercentage] = useState(
|
||||
initialUserData ? 0 : targetXpPercentage
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Si on a des données initiales, animer depuis 0 vers la valeur cible
|
||||
if (initialUserData) {
|
||||
const hpTimer = setTimeout(() => {
|
||||
setHpPercentage(targetHpPercentage);
|
||||
}, 100);
|
||||
|
||||
const xpTimer = setTimeout(() => {
|
||||
setXpPercentage(targetXpPercentage);
|
||||
}, 200);
|
||||
|
||||
return () => {
|
||||
clearTimeout(hpTimer);
|
||||
clearTimeout(xpTimer);
|
||||
};
|
||||
}
|
||||
// Sinon, mettre à jour directement (pour les pages Client Components)
|
||||
// Utiliser requestAnimationFrame pour éviter les cascades de rendu
|
||||
requestAnimationFrame(() => {
|
||||
setHpPercentage(targetHpPercentage);
|
||||
setXpPercentage(targetXpPercentage);
|
||||
});
|
||||
}, [targetHpPercentage, targetXpPercentage, initialUserData]);
|
||||
|
||||
const hpColor =
|
||||
hpPercentage > 60
|
||||
? "from-green-600 to-green-700"
|
||||
: hpPercentage > 30
|
||||
? "from-yellow-600 to-orange-700"
|
||||
: "from-red-700 to-red-900";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Avatar */}
|
||||
<Link
|
||||
href="/profile"
|
||||
className="cursor-pointer hover:opacity-80 transition-opacity"
|
||||
>
|
||||
<Avatar
|
||||
src={avatar}
|
||||
username={username}
|
||||
size="md"
|
||||
borderClassName="border-pixel-gold/20"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex flex-col gap-1.5 min-w-[180px] sm:min-w-[200px]">
|
||||
{/* Username & Level */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href="/profile"
|
||||
className="cursor-pointer hover:text-pixel-gold/80 transition-colors"
|
||||
>
|
||||
<div className="text-pixel-gold font-gaming font-bold text-sm tracking-wider">
|
||||
{username}
|
||||
</div>
|
||||
</Link>
|
||||
<div className="text-gray-400 font-pixel text-xs uppercase border border-pixel-gold/30 px-1.5 py-0.5 bg-black/40">
|
||||
Lv.{level}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bars side by side */}
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
{/* HP Bar */}
|
||||
<div className="relative h-2 flex-1 bg-gray-900 border border-gray-700 rounded overflow-hidden">
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-r ${hpColor} transition-all duration-1000 ease-out`}
|
||||
style={{ width: `${hpPercentage}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
|
||||
</div>
|
||||
{hpPercentage < 30 && (
|
||||
<div className="absolute inset-0 border border-red-500 rounded animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* XP Bar */}
|
||||
<div className="relative h-2 flex-1 bg-gray-900 border border-pixel-gold/30 rounded overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 bg-gradient-to-r from-pixel-gold/80 via-pixel-gold/70 to-pixel-gold/80 transition-all duration-1000 ease-out"
|
||||
style={{ width: `${xpPercentage}%` }}
|
||||
>
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent animate-shimmer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Labels */}
|
||||
<div className="flex items-center gap-2 text-[8px] font-pixel text-gray-400">
|
||||
<div className="flex-1 text-left">
|
||||
HP {hp} / {maxHp}
|
||||
</div>
|
||||
<div className="flex-1 text-right">
|
||||
XP {formatNumber(xp)} / {formatNumber(maxXp)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>{`
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
.animate-shimmer {
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
529
components/profile/ProfileForm.tsx
Normal file
529
components/profile/ProfileForm.tsx
Normal file
@@ -0,0 +1,529 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, useTransition, type ChangeEvent } from "react";
|
||||
import { Avatar, Input, Textarea, Button, Alert, Card, BackgroundSection, SectionTitle, ProgressBar } from "@/components/ui";
|
||||
import { updateProfile } from "@/actions/profile/update-profile";
|
||||
import { updatePassword } from "@/actions/profile/update-password";
|
||||
|
||||
type CharacterClass =
|
||||
| "WARRIOR"
|
||||
| "MAGE"
|
||||
| "ROGUE"
|
||||
| "RANGER"
|
||||
| "PALADIN"
|
||||
| "ENGINEER"
|
||||
| "MERCHANT"
|
||||
| "SCHOLAR"
|
||||
| "BERSERKER"
|
||||
| "NECROMANCER"
|
||||
| null;
|
||||
|
||||
interface UserProfile {
|
||||
id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
bio: string | null;
|
||||
characterClass: CharacterClass;
|
||||
hp: number;
|
||||
maxHp: number;
|
||||
xp: number;
|
||||
maxXp: number;
|
||||
level: number;
|
||||
score: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
interface ProfileFormProps {
|
||||
initialProfile: UserProfile;
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
const formatNumber = (num: number): string => {
|
||||
return num.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
export default function ProfileForm({
|
||||
initialProfile,
|
||||
backgroundImage,
|
||||
}: ProfileFormProps) {
|
||||
const [profile, setProfile] = useState<UserProfile>(initialProfile);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const [username, setUsername] = useState(initialProfile.username);
|
||||
const [avatar, setAvatar] = useState<string | null>(initialProfile.avatar);
|
||||
const [bio, setBio] = useState<string | null>(initialProfile.bio || null);
|
||||
const [characterClass, setCharacterClass] = useState<CharacterClass>(
|
||||
initialProfile.characterClass || null
|
||||
);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState(false);
|
||||
|
||||
// Password change form state
|
||||
const [showPasswordForm, setShowPasswordForm] = useState(false);
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [isChangingPassword, startPasswordTransition] = useTransition();
|
||||
|
||||
const handleAvatarUpload = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
setUploadingAvatar(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", file);
|
||||
|
||||
const response = await fetch("/api/profile/avatar", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAvatar(data.url);
|
||||
setSuccess("Avatar mis à jour avec succès");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.error || "Erreur lors de l'upload de l'avatar");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error uploading avatar:", err);
|
||||
setError("Erreur lors de l'upload de l'avatar");
|
||||
} finally {
|
||||
setUploadingAvatar(false);
|
||||
if (fileInputRef.current) {
|
||||
fileInputRef.current.value = "";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await updateProfile({
|
||||
username,
|
||||
avatar,
|
||||
bio,
|
||||
characterClass,
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
setProfile({
|
||||
...result.data,
|
||||
createdAt: result.data.createdAt instanceof Date
|
||||
? result.data.createdAt.toISOString()
|
||||
: result.data.createdAt,
|
||||
} as UserProfile);
|
||||
setBio(result.data.bio || null);
|
||||
setCharacterClass(result.data.characterClass as CharacterClass || null);
|
||||
setSuccess("Profil mis à jour avec succès");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} else {
|
||||
setError(result.error || "Erreur lors de la mise à jour");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handlePasswordChange = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
startPasswordTransition(async () => {
|
||||
const result = await updatePassword({
|
||||
currentPassword,
|
||||
newPassword,
|
||||
confirmPassword,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setSuccess(result.message || "Mot de passe modifié avec succès");
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
setShowPasswordForm(false);
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
} else {
|
||||
setError(result.error || "Erreur lors de la modification du mot de passe");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
<div className="w-full max-w-4xl mx-auto px-8">
|
||||
{/* Title Section */}
|
||||
<SectionTitle variant="gradient" size="lg" subtitle="Gérez votre profil" className="mb-12">
|
||||
PROFIL
|
||||
</SectionTitle>
|
||||
|
||||
{/* Profile Card */}
|
||||
<Card variant="default" className="overflow-hidden">
|
||||
<form onSubmit={handleSubmit} className="p-8 space-y-8">
|
||||
{/* Messages */}
|
||||
{error && <Alert variant="error">{error}</Alert>}
|
||||
{success && <Alert variant="success">{success}</Alert>}
|
||||
|
||||
{/* Avatar Section */}
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
src={avatar}
|
||||
username={username}
|
||||
size="2xl"
|
||||
borderClassName="border-4 border-pixel-gold/50"
|
||||
/>
|
||||
{uploadingAvatar && (
|
||||
<div className="absolute inset-0 bg-black/60 flex items-center justify-center rounded-full">
|
||||
<div className="text-pixel-gold text-sm">Upload...</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Avatars par défaut */}
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<label className="block text-pixel-gold text-xs uppercase tracking-widest mb-2">
|
||||
Avatars par défaut
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
{[
|
||||
"/avatar-1.jpg",
|
||||
"/avatar-2.jpg",
|
||||
"/avatar-3.jpg",
|
||||
"/avatar-4.jpg",
|
||||
"/avatar-5.jpg",
|
||||
"/avatar-6.jpg",
|
||||
].map((defaultAvatar) => (
|
||||
<button
|
||||
key={defaultAvatar}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setAvatar(defaultAvatar);
|
||||
setSuccess("Avatar sélectionné");
|
||||
setTimeout(() => setSuccess(null), 3000);
|
||||
}}
|
||||
className={`w-16 h-16 rounded-full border-2 overflow-hidden transition ${
|
||||
avatar === defaultAvatar
|
||||
? "border-pixel-gold scale-110"
|
||||
: "border-pixel-gold/30 hover:border-pixel-gold/50"
|
||||
}`}
|
||||
>
|
||||
<img
|
||||
src={defaultAvatar}
|
||||
alt="Avatar par défaut"
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleAvatarUpload}
|
||||
className="hidden"
|
||||
id="avatar-upload"
|
||||
/>
|
||||
<label htmlFor="avatar-upload">
|
||||
<Button variant="primary" size="md" as="span" className="cursor-pointer">
|
||||
{uploadingAvatar ? "Upload en cours..." : "Upload un avatar custom"}
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Username Field */}
|
||||
<Input
|
||||
type="text"
|
||||
label="Nom d'utilisateur"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">3-20 caractères</p>
|
||||
|
||||
{/* Bio Field */}
|
||||
<Textarea
|
||||
label="Bio"
|
||||
value={bio || ""}
|
||||
onChange={(e) => setBio(e.target.value)}
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCharCount
|
||||
placeholder="Parlez-nous de vous..."
|
||||
className="bg-black/40"
|
||||
/>
|
||||
|
||||
{/* Character Class Selection */}
|
||||
<div>
|
||||
<label className="block text-pixel-gold text-sm uppercase tracking-widest mb-3">
|
||||
Classe de Personnage
|
||||
</label>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
||||
{[
|
||||
{
|
||||
value: "WARRIOR",
|
||||
name: "Guerrier",
|
||||
icon: "⚔️",
|
||||
desc: "Maître du combat au corps à corps",
|
||||
},
|
||||
{
|
||||
value: "MAGE",
|
||||
name: "Mage",
|
||||
icon: "🔮",
|
||||
desc: "Manipulateur des arcanes",
|
||||
},
|
||||
{
|
||||
value: "ROGUE",
|
||||
name: "Voleur",
|
||||
icon: "🗡️",
|
||||
desc: "Furtif et mortel",
|
||||
},
|
||||
{
|
||||
value: "RANGER",
|
||||
name: "Rôdeur",
|
||||
icon: "🏹",
|
||||
desc: "Chasseur des terres sauvages",
|
||||
},
|
||||
{
|
||||
value: "PALADIN",
|
||||
name: "Paladin",
|
||||
icon: "🛡️",
|
||||
desc: "Protecteur sacré",
|
||||
},
|
||||
{
|
||||
value: "ENGINEER",
|
||||
name: "Ingénieur",
|
||||
icon: "⚙️",
|
||||
desc: "Créateur d'artefacts",
|
||||
},
|
||||
{
|
||||
value: "MERCHANT",
|
||||
name: "Marchand",
|
||||
icon: "💰",
|
||||
desc: "Maître du commerce",
|
||||
},
|
||||
{
|
||||
value: "SCHOLAR",
|
||||
name: "Érudit",
|
||||
icon: "📚",
|
||||
desc: "Gardien du savoir",
|
||||
},
|
||||
{
|
||||
value: "BERSERKER",
|
||||
name: "Berserker",
|
||||
icon: "🔥",
|
||||
desc: "Rage destructrice",
|
||||
},
|
||||
{
|
||||
value: "NECROMANCER",
|
||||
name: "Nécromancien",
|
||||
icon: "💀",
|
||||
desc: "Maître des morts",
|
||||
},
|
||||
].map((cls) => (
|
||||
<button
|
||||
key={cls.value}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCharacterClass(cls.value as CharacterClass)
|
||||
}
|
||||
className={`p-4 border-2 rounded-lg text-left transition-all ${
|
||||
characterClass === cls.value
|
||||
? "border-pixel-gold bg-pixel-gold/20 shadow-lg shadow-pixel-gold/30"
|
||||
: "border-pixel-gold/30 bg-black/40 hover:border-pixel-gold/50 hover:bg-black/60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-2xl">{cls.icon}</span>
|
||||
<span
|
||||
className={`font-bold text-sm uppercase tracking-wider ${
|
||||
characterClass === cls.value
|
||||
? "text-pixel-gold"
|
||||
: "text-white"
|
||||
}`}
|
||||
>
|
||||
{cls.name}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 leading-tight">
|
||||
{cls.desc}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{characterClass && (
|
||||
<p className="text-pixel-gold text-xs mt-2 uppercase tracking-widest">
|
||||
✓ Classe sélectionnée
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stats Display */}
|
||||
<div className="border-t border-pixel-gold/20 pt-6">
|
||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest mb-4">
|
||||
Statistiques
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4 mb-4">
|
||||
<div className="bg-black/40 border border-pixel-gold/20 rounded p-4">
|
||||
<div className="text-gray-400 text-xs uppercase mb-1">
|
||||
Score
|
||||
</div>
|
||||
<div className="text-pixel-gold text-xl font-bold">
|
||||
{formatNumber(profile.score)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-black/40 border border-pixel-gold/20 rounded p-4">
|
||||
<div className="text-gray-400 text-xs uppercase mb-1">
|
||||
Niveau
|
||||
</div>
|
||||
<div className="text-pixel-gold text-xl font-bold">
|
||||
Lv.{profile.level}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* HP Bar */}
|
||||
<ProgressBar
|
||||
value={profile.hp}
|
||||
max={profile.maxHp}
|
||||
variant="hp"
|
||||
showLabel
|
||||
label="HP"
|
||||
className="mb-4"
|
||||
/>
|
||||
|
||||
{/* XP Bar */}
|
||||
<ProgressBar
|
||||
value={profile.xp}
|
||||
max={profile.maxXp}
|
||||
variant="xp"
|
||||
showLabel
|
||||
label="XP"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Email (read-only) */}
|
||||
<div>
|
||||
<label className="block text-gray-500 text-sm uppercase tracking-widest mb-2">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={profile.email}
|
||||
disabled
|
||||
className="w-full px-4 py-3 bg-black/20 border border-gray-700/50 rounded text-gray-500 cursor-not-allowed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end gap-4 pt-4 border-t border-pixel-gold/20">
|
||||
<Button type="submit" variant="primary" size="md" disabled={isPending}>
|
||||
{isPending ? "Enregistrement..." : "Enregistrer les modifications"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* Password Change Section - Separate form */}
|
||||
<div className="border-t border-pixel-gold/20 p-8">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-pixel-gold text-sm uppercase tracking-widest">
|
||||
Mot de passe
|
||||
</h3>
|
||||
{!showPasswordForm && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
size="md"
|
||||
onClick={() => setShowPasswordForm(true)}
|
||||
>
|
||||
Changer le mot de passe
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showPasswordForm && (
|
||||
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||
<Input
|
||||
type="password"
|
||||
label="Mot de passe actuel"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
required
|
||||
className="bg-black/40"
|
||||
/>
|
||||
|
||||
<Input
|
||||
type="password"
|
||||
label="Nouveau mot de passe"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
<p className="text-gray-500 text-xs mt-1">
|
||||
Minimum 6 caractères
|
||||
</p>
|
||||
|
||||
<Input
|
||||
type="password"
|
||||
label="Confirmer le nouveau mot de passe"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
minLength={6}
|
||||
className="bg-black/40"
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="md"
|
||||
onClick={() => {
|
||||
setShowPasswordForm(false);
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
setConfirmPassword("");
|
||||
setError(null);
|
||||
}}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
size="md"
|
||||
disabled={isChangingPassword}
|
||||
>
|
||||
{isChangingPassword
|
||||
? "Modification..."
|
||||
: "Modifier le mot de passe"}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</BackgroundSection>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user