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:
@@ -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' })}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
248
src/components/ui-showcase/sections/AvatarSection.tsx
Normal file
248
src/components/ui-showcase/sections/AvatarSection.tsx
Normal 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'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'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'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'avatar</div>
|
||||
<div><span className="text-[var(--accent)]">email?</span> - Email pour Gravatar</div>
|
||||
<div><span className="text-[var(--accent)]">name?</span> - Nom d'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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export { AvatarSection } from './AvatarSection';
|
||||
export { ButtonsSection } from './ButtonsSection';
|
||||
export { BadgesSection } from './BadgesSection';
|
||||
export { CardsSection } from './CardsSection';
|
||||
|
||||
49
src/components/ui/Avatar.tsx
Normal file
49
src/components/ui/Avatar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user