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 */}
-

-
-
-
-
- ) : (
-
-
+
+
+ {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 */}
-

{
- e.currentTarget.style.display = 'none'
- }}
- />
+
+ {/* Option Gravatar */}
+
+
+
+ {profile.email && (
+ /* eslint-disable-next-line @next/next/no-img-element */
+

+ )}
+
+
+
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 */}
+

{
+ 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
-

- ) : (
-
- )}
+