'use client'
import { useState, useRef, useEffect } from 'react'
import { useAuth } from '@/components/AuthProvider'
import { useProfile } from '@/lib/hooks/useProfile'
import { supabase } from '@/lib/supabase'
import {
XMarkIcon,
PhotoIcon,
UserIcon,
CheckIcon,
ExclamationTriangleIcon
} from '@heroicons/react/24/outline'
import { createPortal } from 'react-dom'
import Image from 'next/image'
interface ProfileEditDialogProps {
isOpen: boolean
onClose: () => void
}
export default function ProfileEditDialog({ isOpen, onClose }: ProfileEditDialogProps) {
const { user } = useAuth()
const { profile, updateProfile, refetch } = useProfile()
const [fullName, setFullName] = useState('')
const [uploading, setUploading] = useState(false)
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error', text: string } | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
// Update fullName when profile changes
useEffect(() => {
setFullName(profile?.full_name || '')
}, [profile?.full_name])
if (!isOpen) return null
const getLoginProvider = () => {
// Check app_metadata for provider info
const appMetadata = user?.app_metadata
const userMetadata = user?.user_metadata
// Provider is usually in app_metadata.provider
if (appMetadata?.provider) {
return appMetadata.provider
}
// If no explicit provider, try to infer from metadata structure
if (userMetadata?.picture && userMetadata?.picture.includes('googleusercontent.com')) {
return 'google'
}
if (userMetadata?.avatar_url && userMetadata?.avatar_url.includes('avatars.githubusercontent.com')) {
return 'github'
}
// Fallback to checking identities
if (user?.identities && user.identities.length > 0) {
return user.identities[0].provider
}
return 'email'
}
const getProviderIcon = (provider: string) => {
switch (provider) {
case 'google':
return (
<div className="w-5 h-5 bg-white rounded-full flex items-center justify-center">
<svg viewBox="0 0 24 24" className="w-3 h-3">
<path fill="#4285f4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34a853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#fbbc05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#ea4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
</div>
)
case 'github':
return (
<div className="w-5 h-5 bg-gray-900 rounded-full flex items-center justify-center">
<svg viewBox="0 0 24 24" className="w-3 h-3 fill-white">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
</div>
)
default:
return <UserIcon className="w-5 h-5 text-gray-400" />
}
}
const getProviderName = (provider: string) => {
switch (provider) {
case 'google': return 'Google'
case 'github': return 'GitHub'
case 'email': return 'Email'
default: return provider.charAt(0).toUpperCase() + provider.slice(1)
}
}
const getCurrentAvatar = () => {
const profileAvatar = profile?.avatar_url
const authAvatar = user?.user_metadata?.avatar_url || user?.user_metadata?.picture
if (profileAvatar && profileAvatar.trim() !== '') {
return profileAvatar
}
if (authAvatar && authAvatar.trim() !== '') {
return authAvatar
}
return null
}
const getInitials = () => {
const name = fullName || profile?.full_name || user?.user_metadata?.full_name
if (name) {
return name
.split(' ')
.map((n: string) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
}
return user?.email?.slice(0, 2).toUpperCase() || '??'
}
const handleAvatarUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) return
setUploading(true)
setMessage(null)
try {
const formData = new FormData()
formData.append('file', file)
// Get the current session token
const { data: { session } } = await supabase.auth.getSession()
if (!session) {
throw new Error('Not authenticated')
}
const response = await fetch('/api/upload/avatar', {
method: 'POST',
headers: {
'Authorization': `Bearer ${session.access_token}`
},
body: formData,
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Upload failed')
}
setMessage({ type: 'success', text: 'Avatar updated successfully!' })
refetch() // Refresh profile data
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Failed to upload avatar'
})
} finally {
setUploading(false)
}
}
const handleSaveName = async () => {
if (!fullName.trim()) return
setSaving(true)
setMessage(null)
try {
await updateProfile({ full_name: fullName.trim() })
setMessage({ type: 'success', text: 'Name updated successfully!' })
} catch (error) {
setMessage({
type: 'error',
text: error instanceof Error ? error.message : 'Failed to update name'
})
} finally {
setSaving(false)
}
}
const provider = getLoginProvider()
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={onClose}
/>
<div className="relative bg-gray-900/95 backdrop-blur-sm border border-gray-800/50 rounded-2xl p-6 w-full max-w-md shadow-2xl">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl font-bold text-white">Edit Profile</h2>
<button
onClick={onClose}
className="p-2 text-gray-400 hover:text-white hover:bg-gray-800/50 rounded-lg transition-colors"
>
<XMarkIcon className="w-5 h-5" />
</button>
</div>
{/* Avatar Section */}
<div className="text-center mb-6">
<div className="relative inline-block">
{getCurrentAvatar() ? (
<Image
src={getCurrentAvatar()}
alt="Profile"
width={80}
height={80}
className="w-20 h-20 rounded-2xl object-cover border-2 border-gray-700"
onError={(e) => {
e.currentTarget.style.display = 'none'
const fallback = e.currentTarget.nextElementSibling as HTMLElement
if (fallback) fallback.style.display = 'flex'
}}
/>
) : null}
<div
className={`${getCurrentAvatar() ? 'hidden' : 'flex'} w-20 h-20 rounded-2xl bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 items-center justify-center border-2 border-gray-700`}
>
<span className="text-white font-black text-lg">{getInitials()}</span>
</div>
{/* Upload button */}
<button
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
className="absolute -bottom-2 -right-2 p-2 bg-teal-500 hover:bg-teal-600 disabled:bg-gray-600 text-white rounded-full shadow-lg transition-colors"
>
{uploading ? (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" />
) : (
<PhotoIcon className="w-4 h-4" />
)}
</button>
</div>
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleAvatarUpload}
className="hidden"
/>
<p className="text-gray-400 text-sm mt-2">
Click the camera icon to upload a new avatar
</p>
{getCurrentAvatar() && getCurrentAvatar()?.includes('supabase') && (
<p className="text-teal-400 text-xs mt-1">
✨ Custom avatar
</p>
)}
{getCurrentAvatar() && !getCurrentAvatar()?.includes('supabase') && (
<p className="text-blue-400 text-xs mt-1">
🔗 {getProviderName(getLoginProvider())} avatar
</p>
)}
</div>
{/* Name Section */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">
Full Name
</label>
<div className="flex gap-2">
<input
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
placeholder="Enter your full name"
className="flex-1 px-3 py-2 bg-gray-800/50 border border-gray-700 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent"
/>
<button
onClick={handleSaveName}
disabled={saving || !fullName.trim() || fullName.trim() === profile?.full_name}
className="px-4 py-2 bg-teal-500 hover:bg-teal-600 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg transition-colors flex items-center"
>
{saving ? (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-white border-t-transparent" />
) : (
<CheckIcon className="w-4 h-4" />
)}
</button>
</div>
</div>
{/* Login Provider Section */}
<div className="mb-6">
<label className="block text-sm font-medium text-gray-300 mb-2">
Signed in with
</label>
<div className="flex items-center gap-3 px-3 py-2 bg-gray-800/30 border border-gray-700/50 rounded-lg">
{getProviderIcon(provider)}
<span className="text-white font-medium">{getProviderName(provider)}</span>
<div className="ml-auto px-2 py-1 bg-gray-700/50 text-gray-300 text-xs rounded">
{user?.email}
</div>
</div>
</div>
{/* Message */}
{message && (
<div className={`p-3 rounded-lg mb-4 flex items-center gap-2 ${
message.type === 'success'
? 'bg-green-500/20 border border-green-500/30 text-green-300'
: 'bg-red-500/20 border border-red-500/30 text-red-300'
}`}>
{message.type === 'success' ? (
<CheckIcon className="w-4 h-4 flex-shrink-0" />
) : (
<ExclamationTriangleIcon className="w-4 h-4 flex-shrink-0" />
)}
<span className="text-sm">{message.text}</span>
</div>
)}
{/* Footer */}
<div className="flex gap-3">
<button
onClick={onClose}
className="flex-1 px-4 py-2 text-gray-300 bg-gray-800/50 hover:bg-gray-700/50 border border-gray-700 rounded-lg transition-colors"
>
Close
</button>
</div>
</div>
</div>,
document.body
)
}