feat: enhance avatar handling and update TODO.md

- Added Avatar component with support for custom URLs and Gravatar integration, improving user profile visuals.
- Implemented logic to determine avatar source based on user preferences in profile actions.
- Updated ProfilePage to utilize the new Avatar component for better consistency.
- Marked the integration of Gravatar and custom avatar handling as complete in TODO.md.
This commit is contained in:
Julien Froidefond
2025-10-04 11:35:08 +02:00
parent ad0b723e00
commit ffd3eb998a
11 changed files with 711 additions and 51 deletions

View File

@@ -3,7 +3,8 @@
import { useSession, signOut } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { Button } from '@/components/ui/Button'
import { User, LogOut } from 'lucide-react'
import { Avatar } from '@/components/ui/Avatar'
import { LogOut } from 'lucide-react'
export function AuthButton() {
const { data: session, status } = useSession()
@@ -37,16 +38,13 @@ export function AuthButton() {
className="p-1 h-auto"
title={`Profil - ${session.user?.email}`}
>
{session.user?.avatar ? (
// eslint-disable-next-line @next/next/no-img-element
<img
src={session.user.avatar}
alt="Avatar"
className="w-10 h-10 rounded-full object-cover"
/>
) : (
<User className="w-6 h-6" />
)}
<Avatar
url={session.user?.avatar}
email={session.user?.email}
name={session.user?.name}
size={40}
className="w-10 h-10"
/>
</Button>
<Button
onClick={() => signOut({ callbackUrl: '/login' })}

View File

@@ -3,6 +3,7 @@
import { Header } from '@/components/ui/Header';
import { TableOfContents } from './TableOfContents';
import {
AvatarSection,
ButtonsSection,
BadgesSection,
CardsSection,
@@ -34,6 +35,7 @@ export function UIShowcaseClient() {
<ButtonsSection />
<BadgesSection />
<CardsSection />
<AvatarSection />
<DropdownsSection />
<FormsSection />
<NavigationSection />

View File

@@ -0,0 +1,248 @@
'use client';
import { Avatar } from '@/components/ui/Avatar';
export function AvatarSection() {
const sampleUsers = [
{
name: 'Alain Dubois',
email: 'alain.dubois@example.com',
avatar: 'https://images.unsplash.com/photo-1472099645785-5658abf4ff4e?w=150&h=150&fit=crop&crop=face'
},
{
name: 'Sophie Martin',
email: 'sophie.martin@example.com',
avatar: null
},
{
name: 'Pierre Durand',
email: 'pierre.durand@example.com',
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150&h=150&fit=crop&crop=face'
},
{
name: 'Marie Leclerc',
email: 'marie.leclerc@example.com',
avatar: null
},
{
name: 'Thomas Bernard',
email: 'thomas.bernard@gravatar.com',
avatar: 'https://www.gravatar.com/avatar/205e460b479e2e5b48aec07710c08d50?s=200&d=identicon&r=g'
}
];
return (
<section id="avatar-section" className="space-y-8">
<div>
<h2 className="text-3xl font-mono font-bold text-[var(--foreground)] mb-4">Avatar</h2>
<p className="text-[var(--muted-foreground)] mb-6">
Composant d&apos;avatar avec support automatique Gravatar, URLs personnalisées et fallback intelligent.
</p>
</div>
{/* Exemples de base */}
<div className="space-y-6">
<h3 className="text-xl font-mono font-semibold text-[var(--foreground)]">Tailles disponibles</h3>
<div className="flex flex-wrap items-center gap-4">
{[24, 32, 40, 48, 64, 80, 96].map((size) => (
<div key={size} className="text-center">
<Avatar
name="Test User"
email="test@example.com"
size={size}
className="mb-2"
/>
<div className="text-xs text-[var(--muted-foreground)]">{size}px</div>
</div>
))}
</div>
</div>
{/* Différents types d'avatar */}
<div className="space-y-6">
<h3 className="text-xl font-mono font-semibold text-[var(--foreground)]">Types d&apos;avatar</h3>
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sampleUsers.map((user, index) => (
<div key={index} className="flex flex-col items-center space-y-3 p-4 bg-[var(--input)] rounded-lg border border-[var(--border)]">
<Avatar
url={user.avatar}
email={user.email}
name={user.name}
size={60}
className="mb-2"
/>
<div className="text-center">
<div className="font-medium text-[var(--foreground)] text-sm">{user.name}</div>
<div className="text-xs text-[var(--muted-foreground)]">{user.email}</div>
<div className="mt-2 text-xs">
{user.avatar ? (
user.avatar.includes('gravatar.com') ? (
<span className="px-2 py-1 bg-[var(--primary)]/20 text-[var(--primary)] rounded-full">
🔗 Gravatar
</span>
) : (
<span className="px-2 py-1 bg-[var(--accent)]/20 text-[var(--accent)] rounded-full">
🖼 Custom URL
</span>
)
) : (
<span className="px-2 py-1 bg-[var(--muted)]/20 text-[var(--muted-foreground)] rounded-full">
👤 Fallback
</span>
)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* Utilisation dans différents contextes */}
<div className="space-y-6">
<h3 className="text-xl font-mono font-semibold text-[var(--foreground)]">Utilisation dans différents contextes</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Liste d'utilisateurs */}
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
<h4 className="font-medium text-[var(--foreground)] mb-4">Liste d&apos;utilisateurs</h4>
<div className="space-y-3">
{sampleUsers.slice(0, 3).map((user, index) => (
<div key={index} className="flex items-center gap-3 p-2 hover:bg-[var(--input)] rounded-md transition-colors">
<Avatar
url={user.avatar}
email={user.email}
name={user.name}
size={32}
/>
<div className="flex-1">
<div className="font-medium text-[var(--foreground)] text-sm">{user.name}</div>
<div className="text-xs text-[var(--muted-foreground)]">{user.email}</div>
</div>
<div className="text-xs text-[var(--muted-foreground)]">
{user.avatar ? 'Connecté' : 'Absent'}
</div>
</div>
))}
</div>
</div>
{/* Notification avec avatar */}
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
<h4 className="font-medium text-[var(--foreground)] mb-4">Notifications</h4>
<div className="space-y-4">
<div className="flex items-start gap-3 p-3 bg-[var(--primary)]/10 rounded-lg border border-[var(--primary)]/20">
<Avatar
url={sampleUsers[0].avatar}
email={sampleUsers[0].email}
name={sampleUsers[0].name}
size={40}
/>
<div className="flex-1">
<div className="font-medium text-[var(--foreground)] text-sm">
{sampleUsers[0].name} a terminé une tâche
</div>
<div className="text-xs text-[var(--muted-foreground)] mt-1">
Il y a 5 minutes
</div>
</div>
</div>
<div className="flex items-start gap-3 p-3 bg-[var(--accent)]/10 rounded-lg border border-[var(--accent)]/20">
<Avatar
url={sampleUsers[1].avatar}
email={sampleUsers[1].email}
name={sampleUsers[1].name}
size={40}
/>
<div className="flex-1">
<div className="font-medium text-[var(--foreground)] text-sm">
{sampleUsers[1].name} vous a envoyé un message
</div>
<div className="text-xs text-[var(--muted-foreground)] mt-1">
Il y a 12 heures
</div>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Code exemple */}
<div className="space-y-4">
<h3 className="text-xl font-mono font-semibold text-[var(--foreground)]">Utilisation</h3>
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)] font-mono text-sm">
<pre className="text-[var(--foreground)] overflow-x-auto">
{`// Avatar avec URL personnalisée
<Avatar
url="https://example.com/photo.jpg"
email="user@example.com"
name="John Doe"
size={64}
/>
// Avatar Gravatar automatique
<Avatar
email="user@gravatar.com"
name="Jane Doe"
size={48}
/>
// Avatar avec fallback
<Avatar
email="unknown@example.com"
name="Unknown User"
size={32}
/>
// Avatar avec icône par défaut seulement
<Avatar
name="Guest"
size={40}
/>`}
</pre>
</div>
</div>
{/* Props disponibles */}
<div className="space-y-4">
<h3 className="text-xl font-mono font-semibold text-[var(--foreground)]">Props</h3>
<div className="bg-[var(--card)] rounded-xl p-6 border border-[var(--border)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 font-mono text-sm">
<div>
<div className="font-semibold text-[var(--primary)] mb-2">Props principales</div>
<div className="space-y-1 text-[var(--foreground)]">
<div><span className="text-[var(--accent)]">url?</span> - URL de l&apos;avatar</div>
<div><span className="text-[var(--accent)]">email?</span> - Email pour Gravatar</div>
<div><span className="text-[var(--accent)]">name?</span> - Nom d&apos;affichage</div>
<div><span className="text-[var(--accent)]">size?</span> - Taille en pixels (défaut: 40)</div>
<div><span className="text-[var(--accent)]">className?</span> - Classes CSS supplémentaires</div>
</div>
</div>
<div>
<div className="font-semibold text-[var(--success)] mb-2">Comportement</div>
<div className="space-y-1 text-[var(--foreground)]">
<div> Support automatique Gravatar</div>
<div> Validation URLs HTTPS</div>
<div> Détection intelligente du type</div>
<div> Fallback avec icône User</div>
<div> Cache et optimisations</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@@ -1,3 +1,4 @@
export { AvatarSection } from './AvatarSection';
export { ButtonsSection } from './ButtonsSection';
export { BadgesSection } from './BadgesSection';
export { CardsSection } from './CardsSection';

View File

@@ -0,0 +1,49 @@
'use client'
import { User } from 'lucide-react'
import { getGravatarUrl, isGravatarUrl } from '@/lib/gravatar'
interface AvatarProps {
url?: string | null
email?: string
name?: string
size?: number
className?: string
}
export function Avatar({ url, email, name, size = 40, className = '' }: AvatarProps) {
// Déterminer l'URL de l'avatar à utiliser
let avatarUrl: string | null = url || null
// Si pas d'avatar mais que c'est peut-être un Gravatar, essayer de le détecter
if (!avatarUrl && email && url && isGravatarUrl(url)) {
avatarUrl = url
}
// Si pas d'URL mais qu'on a un email, utiliser Gravatar par défaut
if (!avatarUrl && email) {
avatarUrl = getGravatarUrl(email, { size })
}
if (avatarUrl) {
return (
/* eslint-disable-next-line @next/next/no-img-element */
<img
src={avatarUrl}
alt={name ? `Avatar de ${name}` : 'Avatar'}
className={`rounded-full object-cover ${className}`}
style={{ width: size, height: size }}
/>
)
}
// Fallback icon
return (
<div
className={`rounded-full bg-[var(--primary)]/20 border-[var(--primary)]/30 flex items-center justify-center ${className}`}
style={{ width: size, height: size }}
>
<User style={{ width: size * 0.5, height: size * 0.5 }} className="text-[var(--primary)]" />
</div>
)
}