diff --git a/TODO.md b/TODO.md index 8a1cf8c..458e4b9 100644 --- a/TODO.md +++ b/TODO.md @@ -41,7 +41,7 @@ ### 🔧 Fonctionnalités et Intégrations - [ ] **Synchro Jira et TFS shortcuts** - Ajouter des raccourcis et bouton dans Kanban -- [ ] **Intégration suppressions Jira/TFS** - Aligner la gestion des suppressions sur TFS, je veux que ce qu'on a récupéré dans la synchro, quand ca devient terminé dans Jira ou TFS, soit marqué comme terminé dans le Kanban et non supprimé du kanban. +- [x] **Intégration suppressions Jira/TFS** - Aligner la gestion des suppressions sur TFS, je veux que ce qu'on a récupéré dans la synchro, quand ca devient terminé dans Jira ou TFS, soit marqué comme terminé dans le Kanban et non supprimé du kanban. - [ ] **Log d'activité** - Implémenter un système de log d'activité (feature potentielle) --- diff --git a/UI_COMPONENTS_GUIDE.md b/UI_COMPONENTS_GUIDE.md index 1f5a08b..32ec17e 100644 --- a/UI_COMPONENTS_GUIDE.md +++ b/UI_COMPONENTS_GUIDE.md @@ -76,6 +76,18 @@ function TaskCard({ task }) { ``` +### Avatar +```tsx +// Avatar avec URL personnalisée + + +// Avatar Gravatar automatique (si pas d'URL fournie) + + +// Avatar avec fallback + +``` + ## 🔄 Migration ### Étape 1: Identifier les patterns diff --git a/src/actions/profile.ts b/src/actions/profile.ts index e4df1ba..30b4b78 100644 --- a/src/actions/profile.ts +++ b/src/actions/profile.ts @@ -4,12 +4,14 @@ import { getServerSession } from 'next-auth/next' import { authOptions } from '@/lib/auth' import { usersService } from '@/services/users' import { revalidatePath } from 'next/cache' +import { getGravatarUrl } from '@/lib/gravatar' export async function updateProfile(formData: { name?: string firstName?: string lastName?: string avatar?: string + useGravatar?: boolean }) { try { const session = await getServerSession(authOptions) @@ -35,12 +37,27 @@ export async function updateProfile(formData: { return { success: false, error: 'L\'URL de l\'avatar ne peut pas dépasser 500 caractères' } } + // Déterminer l'URL de l'avatar + let finalAvatarUrl: string | null = null + + if (formData.useGravatar) { + // Utiliser Gravatar si demandé + finalAvatarUrl = getGravatarUrl(session.user.email || '', { size: 200 }) + } else if (formData.avatar) { + // Utiliser l'URL custom si fournie + finalAvatarUrl = formData.avatar + } else { + // Garder l'avatar actuel ou null + const currentUser = await usersService.getUserById(session.user.id) + finalAvatarUrl = currentUser?.avatar || null + } + // Mettre à jour l'utilisateur const updatedUser = await usersService.updateUser(session.user.id, { name: formData.name || null, firstName: formData.firstName || null, lastName: formData.lastName || null, - avatar: formData.avatar || null, + avatar: finalAvatarUrl, }) // Revalider la page de profil @@ -101,3 +118,47 @@ export async function getProfile() { return { success: false, error: 'Erreur lors de la récupération du profil' } } } + +export async function applyGravatar() { + try { + const session = await getServerSession(authOptions) + + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' } + } + + if (!session.user?.email) { + return { success: false, error: 'Email requis pour Gravatar' } + } + + // Générer l'URL Gravatar + const gravatarUrl = getGravatarUrl(session.user.email, { size: 200 }) + + // Mettre à jour l'utilisateur + const updatedUser = await usersService.updateUser(session.user.id, { + avatar: gravatarUrl, + }) + + // Revalider la page de profil + revalidatePath('/profile') + + return { + success: true, + user: { + id: updatedUser.id, + email: updatedUser.email, + name: updatedUser.name, + firstName: updatedUser.firstName, + lastName: updatedUser.lastName, + avatar: updatedUser.avatar, + role: updatedUser.role, + createdAt: updatedUser.createdAt.toISOString(), + lastLoginAt: updatedUser.lastLoginAt?.toISOString() || null, + } + } + + } catch (error) { + console.error('Gravatar update error:', error) + return { success: false, error: 'Erreur lors de la mise à jour Gravatar' } + } +} diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 1a598d0..21721cd 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -6,8 +6,10 @@ import { useRouter } from 'next/navigation' import { Button } from '@/components/ui/Button' import { Input } from '@/components/ui/Input' import { Header } from '@/components/ui/Header' -import { updateProfile, getProfile } from '@/actions/profile' -import { Check, User, Mail, Calendar, Shield, Save, X, Loader2 } from 'lucide-react' +import { updateProfile, getProfile, applyGravatar } from '@/actions/profile' +import { getGravatarUrl, isGravatarUrl } from '@/lib/gravatar' +import { Check, User, Mail, Calendar, Shield, Save, X, Loader2, Image, ExternalLink } from 'lucide-react' +import { Avatar } from '@/components/ui/Avatar' interface UserProfile { id: string @@ -102,6 +104,41 @@ export default function ProfilePage() { }) } + const handleUseGravatar = async () => { + setError('') + setSuccess('') + + startTransition(async () => { + try { + const result = await applyGravatar() + + if (!result.success || !result.user) { + setError(result.error || 'Erreur lors de la mise à jour Gravatar') + return + } + + setProfile(result.user) + setFormData(prev => ({ ...prev, avatar: result.user!.avatar || '' })) + setSuccess('Avatar Gravatar appliqué avec succès') + + // Mettre à jour la session NextAuth + await update({ + ...session, + user: { + ...session?.user, + name: result.user.name || `${result.user.firstName || ''} ${result.user.lastName || ''}`.trim() || result.user.email, + firstName: result.user.firstName, + lastName: result.user.lastName, + avatar: result.user.avatar, + } + }) + + } catch (error) { + setError(error instanceof Error ? error.message : 'Erreur inconnue') + } + }) + } + const handleChange = (field: string, value: string) => { setFormData(prev => ({ ...prev, [field]: value })) } @@ -146,22 +183,23 @@ export default function ProfilePage() {
{/* Avatar section */} -
- {profile.avatar ? ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - Avatar -
- -
-
- ) : ( -
- +
+ + {profile.avatar && ( +
+ {isGravatarUrl(profile.avatar) ? ( + /* eslint-disable-next-line jsx-a11y/alt-text */ + + ) : ( + /* eslint-disable-next-line jsx-a11y/alt-text */ + + )}
)}
@@ -279,28 +317,86 @@ export default function ProfilePage() {
- handleChange('avatar', e.target.value)} - placeholder="https://example.com/avatar.jpg" - /> - {formData.avatar && ( -
- Aperçu: - {/* eslint-disable-next-line @next/next/no-img-element */} - Aperçu avatar { - e.currentTarget.style.display = 'none' - }} - /> + + {/* Option Gravatar */} +
+
+
+ {profile.email && ( + /* eslint-disable-next-line @next/next/no-img-element */ + Aperçu Gravatar + )} +
+
+
Gravatar
+
+ Utilise l'avatar lié à votre email +
+
+
- )} + + {/* Option URL custom */} +
+
+ + URL personnalisée +
+ handleChange('avatar', e.target.value)} + placeholder="https://example.com/avatar.jpg" + className={isGravatarUrl(formData.avatar) ? 'opacity-50' : ''} + /> +

+ Entrez une URL d'image sécurisée (HTTPS uniquement) +

+ {formData.avatar && !isGravatarUrl(formData.avatar) && ( +
+ Aperçu: + {/* eslint-disable-next-line @next/next/no-img-element */} + Aperçu avatar personnalisé { + e.currentTarget.style.display = 'none' + }} + /> +
+ )} +
+ + {/* Reset button */} + {(formData.avatar && !isGravatarUrl(formData.avatar)) && ( + + )} +
{error && ( diff --git a/src/components/AuthButton.tsx b/src/components/AuthButton.tsx index 8a6eeaa..95b2323 100644 --- a/src/components/AuthButton.tsx +++ b/src/components/AuthButton.tsx @@ -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 - Avatar - ) : ( - - )} +