feat: enhance profile page and authentication with user avatar support
- Updated `next.config.ts` to allow images from various external sources, including LinkedIn and GitHub. - Refactored `ProfilePage` to improve layout and display user avatar, name, and role more prominently. - Enhanced `AuthButton` to show user avatar if available, improving user experience. - Updated authentication logic in `auth.ts` to include user avatar and role in session management. - Extended JWT type definitions to support new user fields (firstName, lastName, avatar, role) for better user data handling.
This commit is contained in:
@@ -2,6 +2,46 @@ import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: 'standalone',
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'media.licdn.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'avatars.githubusercontent.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'lh3.googleusercontent.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'cdn.discordapp.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'images.unsplash.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'via.placeholder.com',
|
||||
port: '',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
turbopack: {
|
||||
rules: {
|
||||
'*.sql': ['raw'],
|
||||
|
||||
@@ -140,138 +140,228 @@ export default function ProfilePage() {
|
||||
<Header title="TowerControl" subtitle="Profil utilisateur" />
|
||||
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="max-w-2xl mx-auto">
|
||||
{/* Informations générales */}
|
||||
<div className="bg-[var(--card)] rounded-lg p-6 mb-6">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
|
||||
Informations générales
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Email
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{profile.email}
|
||||
</div>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
L'email ne peut pas être modifié
|
||||
</p>
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{/* Header avec avatar et infos principales */}
|
||||
<div className="bg-gradient-to-r from-[var(--primary)]/10 to-[var(--accent)]/10 rounded-xl p-8 mb-8 border border-[var(--primary)]/20">
|
||||
<div className="flex flex-col md:flex-row items-center md:items-start gap-6">
|
||||
{/* Avatar section */}
|
||||
<div className="flex-shrink-0">
|
||||
{profile.avatar ? (
|
||||
<div className="relative">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={profile.avatar}
|
||||
alt="Avatar"
|
||||
className="w-24 h-24 rounded-full object-cover border-4 border-[var(--primary)]/30 shadow-lg"
|
||||
/>
|
||||
<div className="absolute -bottom-2 -right-2 w-8 h-8 bg-[var(--success)] rounded-full border-4 border-[var(--card)] flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-white" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-24 h-24 rounded-full bg-[var(--primary)]/20 border-4 border-[var(--primary)]/30 flex items-center justify-center">
|
||||
<svg className="w-12 h-12 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Rôle
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{profile.role}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Membre depuis
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{new Date(profile.createdAt).toLocaleDateString('fr-FR')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{profile.lastLoginAt && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-[var(--muted-foreground)] mb-1">
|
||||
Dernière connexion
|
||||
</label>
|
||||
<div className="text-[var(--foreground)] bg-[var(--input)] px-3 py-2 rounded-md">
|
||||
{new Date(profile.lastLoginAt).toLocaleString('fr-FR')}
|
||||
|
||||
{/* User info */}
|
||||
<div className="flex-1 text-center md:text-left">
|
||||
<h1 className="text-3xl font-mono font-bold text-[var(--foreground)] mb-2">
|
||||
{profile.name || `${profile.firstName || ''} ${profile.lastName || ''}`.trim() || profile.email}
|
||||
</h1>
|
||||
<p className="text-[var(--muted-foreground)] text-lg mb-4">{profile.email}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-4 justify-center md:justify-start">
|
||||
<div className="flex items-center gap-2 px-3 py-1 bg-[var(--card)] rounded-full border border-[var(--border)]">
|
||||
<div className="w-2 h-2 bg-[var(--success)] rounded-full"></div>
|
||||
<span className="text-sm font-medium text-[var(--foreground)]">{profile.role}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-3 py-1 bg-[var(--card)] rounded-full border border-[var(--border)]">
|
||||
<svg className="w-4 h-4 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<span className="text-sm text-[var(--muted-foreground)]">
|
||||
Membre depuis {new Date(profile.createdAt).toLocaleDateString('fr-FR')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Formulaire de modification */}
|
||||
<form onSubmit={handleSubmit} className="bg-[var(--card)] rounded-lg p-6">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-4">
|
||||
Modifier le profil
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Informations générales */}
|
||||
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-6 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Informations générales
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-3 p-3 bg-[var(--input)] rounded-lg border border-[var(--border)]">
|
||||
<svg className="w-5 h-5 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 12a4 4 0 10-8 0 4 4 0 008 0zm0 0v1.5a2.5 2.5 0 005 0V12a9 9 0 10-9 9m4.5-1.206a8.959 8.959 0 01-4.5 1.207" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="text-[var(--foreground)] font-medium">{profile.email}</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)]">Email principal</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Prénom
|
||||
</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleChange('firstName', e.target.value)}
|
||||
placeholder="Votre prénom"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 bg-[var(--input)] rounded-lg border border-[var(--border)]">
|
||||
<svg className="w-5 h-5 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="text-[var(--foreground)] font-medium">{profile.role}</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)]">Rôle utilisateur</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom de famille
|
||||
</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleChange('lastName', e.target.value)}
|
||||
placeholder="Votre nom de famille"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom d'affichage (optionnel)
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="Nom d'affichage personnalisé"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Si vide, sera généré automatiquement à partir du prénom et nom
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="avatar" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
URL de l'avatar (optionnel)
|
||||
</label>
|
||||
<Input
|
||||
id="avatar"
|
||||
value={formData.avatar}
|
||||
onChange={(e) => handleChange('avatar', e.target.value)}
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
/>
|
||||
{profile.lastLoginAt && (
|
||||
<div className="flex items-center gap-3 p-3 bg-[var(--input)] rounded-lg border border-[var(--border)]">
|
||||
<svg className="w-5 h-5 text-[var(--muted-foreground)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="text-[var(--foreground)] font-medium">
|
||||
{new Date(profile.lastLoginAt).toLocaleString('fr-FR')}
|
||||
</div>
|
||||
<div className="text-xs text-[var(--muted-foreground)]">Dernière connexion</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 text-[var(--destructive)] text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
{/* Formulaire de modification */}
|
||||
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
|
||||
<h2 className="text-xl font-mono font-bold text-[var(--foreground)] mb-6 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-[var(--primary)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={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" />
|
||||
</svg>
|
||||
Modifier le profil
|
||||
</h2>
|
||||
|
||||
{success && (
|
||||
<div className="mt-4 text-[var(--success)] text-sm">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label htmlFor="firstName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Prénom
|
||||
</label>
|
||||
<Input
|
||||
id="firstName"
|
||||
value={formData.firstName}
|
||||
onChange={(e) => handleChange('firstName', e.target.value)}
|
||||
placeholder="Votre prénom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full"
|
||||
>
|
||||
{isPending ? 'Sauvegarde...' : 'Sauvegarder les modifications'}
|
||||
</Button>
|
||||
<div>
|
||||
<label htmlFor="lastName" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom de famille
|
||||
</label>
|
||||
<Input
|
||||
id="lastName"
|
||||
value={formData.lastName}
|
||||
onChange={(e) => handleChange('lastName', e.target.value)}
|
||||
placeholder="Votre nom de famille"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="name" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
Nom d'affichage (optionnel)
|
||||
</label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
placeholder="Nom d'affichage personnalisé"
|
||||
/>
|
||||
<p className="text-xs text-[var(--muted-foreground)] mt-1">
|
||||
Si vide, sera généré automatiquement à partir du prénom et nom
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="avatar" className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||
URL de l'avatar (optionnel)
|
||||
</label>
|
||||
<Input
|
||||
id="avatar"
|
||||
value={formData.avatar}
|
||||
onChange={(e) => handleChange('avatar', e.target.value)}
|
||||
placeholder="https://example.com/avatar.jpg"
|
||||
/>
|
||||
{formData.avatar && (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-xs text-[var(--muted-foreground)]">Aperçu:</span>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={formData.avatar}
|
||||
alt="Aperçu avatar"
|
||||
className="w-8 h-8 rounded-full object-cover border border-[var(--border)]"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="flex items-center gap-2 p-3 bg-[var(--destructive)]/10 border border-[var(--destructive)]/30 rounded-lg">
|
||||
<svg className="w-5 h-5 text-[var(--destructive)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-[var(--destructive)] text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="flex items-center gap-2 p-3 bg-[var(--success)]/10 border border-[var(--success)]/30 rounded-lg">
|
||||
<svg className="w-5 h-5 text-[var(--success)]" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span className="text-[var(--success)] text-sm">{success}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-4">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="w-full bg-[var(--primary)] hover:bg-[var(--primary)]/90 text-[var(--primary-foreground)]"
|
||||
>
|
||||
{isPending ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Sauvegarde...
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
Sauvegarder les modifications
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,9 +36,18 @@ export function AuthButton() {
|
||||
className="p-1 h-auto"
|
||||
title={`Profil - ${session.user?.email}`}
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
{session.user?.avatar ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={session.user.avatar}
|
||||
alt="Avatar"
|
||||
className="w-4 h-4 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => signOut({ callbackUrl: '/login' })}
|
||||
|
||||
@@ -59,12 +59,20 @@ export const authOptions: NextAuthOptions = {
|
||||
async jwt({ token, user }) {
|
||||
if (user) {
|
||||
token.id = user.id
|
||||
token.firstName = user.firstName
|
||||
token.lastName = user.lastName
|
||||
token.avatar = user.avatar
|
||||
token.role = user.role
|
||||
}
|
||||
return token
|
||||
},
|
||||
async session({ session, token }) {
|
||||
if (token && session.user) {
|
||||
session.user.id = token.id as string
|
||||
session.user.firstName = token.firstName as string | undefined
|
||||
session.user.lastName = token.lastName as string | undefined
|
||||
session.user.avatar = token.avatar as string | undefined
|
||||
session.user.role = token.role as string
|
||||
}
|
||||
return session
|
||||
},
|
||||
|
||||
4
src/types/next-auth.d.ts
vendored
4
src/types/next-auth.d.ts
vendored
@@ -28,5 +28,9 @@ declare module "next-auth" {
|
||||
declare module "next-auth/jwt" {
|
||||
interface JWT {
|
||||
id: string
|
||||
firstName?: string
|
||||
lastName?: string
|
||||
avatar?: string
|
||||
role: string
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user