bookwiz.io / app / dashboard / profile / page.tsx
page.tsx
Raw
'use client'

import { useState, useEffect, useCallback } from 'react'
import { useAuth } from '@/components/AuthProvider'
import { useProfile } from '@/lib/hooks/useProfile'
import { useSubscription } from '@/lib/hooks/useSubscription'
import { useWelcomeTour } from '@/lib/hooks/useWelcomeTour'
import DashboardLayout from '@/components/dashboard/DashboardLayout'
import ProfileEditDialog from '@/components/ProfileEditDialog'
import WelcomeTour from '@/components/WelcomeTour'
import { 
  SparklesIcon,
  ArrowRightOnRectangleIcon,
  PencilIcon,
  UserCircleIcon,
  AcademicCapIcon,
} from '@heroicons/react/24/outline'
import { formatDate, formatPrice } from '@/lib/stripe'
import { supabase } from '@/lib/supabase'
import Image from 'next/image'

interface EmailPreferences {
  weekly_progress_digest: boolean
  product_updates: boolean
  account_notifications: boolean
}

export default function ProfilePage() {
  const { user, signingOut, signOut } = useAuth()
  const { profile } = useProfile()
  const { 
    subscription, 
    isActive, 
    getCurrentPlan,
    canManageBilling
  } = useSubscription()
  const [emailPreferences, setEmailPreferences] = useState<EmailPreferences>({
    weekly_progress_digest: true,
    product_updates: true,
    account_notifications: true
  })
  const [emailSaving, setEmailSaving] = useState(false)
  const [isEditDialogOpen, setIsEditDialogOpen] = useState(false)
  const { openTour, isOpen: isTourOpen, closeTour, completeTour } = useWelcomeTour()

  const currentPlan = getCurrentPlan()

  const fetchEmailPreferences = useCallback(async () => {
    try {
      const response = await fetch(`/api/profile/email-preferences?userId=${user?.id}`, {
        headers: {
          'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`
        }
      })
      if (response.ok) {
        const data = await response.json()
        setEmailPreferences(data.preferences)
      }
    } catch (error) {
      console.error('Error fetching email preferences:', error)
    }
  }, [user?.id])

  useEffect(() => {
    if (user?.id) {
      fetchEmailPreferences()
    }
  }, [user?.id, fetchEmailPreferences])

  const updateEmailPreferences = async (newPreferences: EmailPreferences) => {
    try {
      setEmailSaving(true)
      const response = await fetch('/api/profile/email-preferences', {
        method: 'POST',
        headers: { 
          'Content-Type': 'application/json',
          'Authorization': `Bearer ${(await supabase.auth.getSession()).data.session?.access_token}`
        },
        body: JSON.stringify({
          userId: user?.id,
          ...newPreferences
        }),
      })
      if (response.ok) {
        setEmailPreferences(newPreferences)
      }
    } catch (error) {
      console.error('Error updating email preferences:', error)
    } finally {
      setEmailSaving(false)
    }
  }

  const handleEmailPreferenceChange = (key: keyof EmailPreferences, value: boolean) => {
    const newPreferences = { ...emailPreferences, [key]: value }
    updateEmailPreferences(newPreferences)
  }

  const getInitials = () => {
    // Use profile data first, then fallback to auth metadata
    const fullName = profile?.full_name || user?.user_metadata?.full_name
    if (fullName) {
      return fullName
        .split(' ')
        .map((name: string) => name[0])
        .join('')
        .toUpperCase()
        .slice(0, 2)
    }
    return user?.email?.slice(0, 2).toUpperCase() || '??'
  }

  const getAvatarUrl = () => {
    // Use profile avatar first, then fallback to auth metadata
    const profileAvatar = profile?.avatar_url
    const authAvatar = user?.user_metadata?.avatar_url || user?.user_metadata?.picture
    
    // Only return a URL if it's not empty/null/undefined
    if (profileAvatar && profileAvatar.trim() !== '') {
      return profileAvatar
    }
    if (authAvatar && authAvatar.trim() !== '') {
      return authAvatar
    }
    return null
  }

  const getDisplayName = () => {
    // Use profile data first, then fallback to auth metadata
    return profile?.full_name || user?.user_metadata?.full_name || 'Your Profile'
  }

  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-4 h-4 bg-white rounded-full flex items-center justify-center">
            <svg viewBox="0 0 24 24" className="w-2.5 h-2.5">
              <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-4 h-4 bg-gray-900 rounded-full flex items-center justify-center">
            <svg viewBox="0 0 24 24" className="w-2.5 h-2.5 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 <UserCircleIcon className="w-4 h-4 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)
    }
  }

  return (
    <DashboardLayout 
      title="Profile" 
      subtitle="Account settings and preferences"
    >
      <div className="space-y-6">
        {/* Profile & Subscription */}
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
          {/* Profile Info */}
          <div className="bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl p-6">
            <div className="flex items-center space-x-4 mb-6">
              <div className="flex-shrink-0 relative">
                {getAvatarUrl() ? (
                  <>
                    <Image 
                      src={getAvatarUrl()} 
                      alt="Profile" 
                      width={48}
                      height={48}
                      className="w-12 h-12 rounded-xl object-cover border-2 border-white/20"
                      onError={(e) => {
                        console.error('Avatar failed to load:', getAvatarUrl())
                        // Hide the image and show the fallback
                        e.currentTarget.style.display = 'none'
                        // Show the fallback div
                        const fallback = e.currentTarget.nextElementSibling as HTMLElement
                        if (fallback) fallback.style.display = 'flex'
                      }}
                    />
                    <div 
                      className="hidden w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 items-center justify-center border-2 border-white/20"
                      style={{ display: 'none' }}
                    >
                      <span className="text-white font-black text-sm">{getInitials()}</span>
                    </div>
                  </>
                ) : (
                  <div className="w-12 h-12 rounded-xl bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 flex items-center justify-center border-2 border-white/20">
                    <span className="text-white font-black text-sm">{getInitials()}</span>
                  </div>
                )}
              </div>
              <div className="flex-1 min-w-0">
                <h3 className="text-lg font-bold text-white truncate">
                  {getDisplayName()}
                </h3>
                <div className="flex items-center gap-2 mb-1">
                  <p className="text-gray-400 text-sm truncate">{user?.email}</p>
                  <div className="flex items-center gap-1 px-2 py-1 bg-gray-800/50 rounded text-xs">
                    {getProviderIcon(getLoginProvider())}
                    <span className="text-gray-300">{getProviderName(getLoginProvider())}</span>
                  </div>
                </div>
                <p className="text-gray-500 text-xs">
                  Member since {formatDate(user?.created_at || '')}
                </p>
              </div>
              <button
                onClick={() => setIsEditDialogOpen(true)}
                className="p-2 text-gray-400 hover:text-white hover:bg-gray-800/50 rounded-lg transition-colors"
                title="Edit Profile"
              >
                <PencilIcon className="w-4 h-4" />
              </button>
            </div>

            <div className="flex gap-2">
              <button 
                onClick={openTour}
                className="flex-1 flex items-center justify-center p-2.5 bg-gray-800/50 hover:bg-gray-700/50 border border-gray-700/50 rounded-lg transition-all text-gray-300 hover:text-white text-sm"
              >
                <AcademicCapIcon className="w-4 h-4 mr-1.5" />
                Tour
              </button>
              <button 
                onClick={signOut}
                disabled={signingOut}
                className="flex-1 flex items-center justify-center p-2.5 bg-gray-800/50 hover:bg-gray-700/50 border border-gray-700/50 rounded-lg transition-all text-gray-300 hover:text-white text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-gray-800/50"
              >
                {signingOut ? (
                  <>
                    <div className="animate-spin rounded-full h-4 w-4 border-2 border-gray-300 border-t-transparent mr-1.5"></div>
                    Signing Out...
                  </>
                ) : (
                  <>
                    <ArrowRightOnRectangleIcon className="w-4 h-4 mr-1.5" />
                    Log Out
                  </>
                )}
              </button>
            </div>
          </div>

          {/* Subscription */}
          <div className="bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl p-6">
            <h3 className="text-lg font-bold text-white mb-4">Subscription</h3>
            <div className="flex items-center space-x-4 mb-4">
              <div className="p-3 bg-gradient-to-br from-teal-500/20 to-cyan-500/20 rounded-lg border border-teal-500/30">
                <SparklesIcon className="h-6 w-6 text-teal-400" />
              </div>
              <div className="flex-1">
                <h4 className="text-white font-medium">
                  {currentPlan ? currentPlan.name : 'Free Plan'}
                </h4>
                <p className="text-gray-400 text-sm">
                  {isActive ? 'Active' : 'No subscription'}
                </p>
              </div>
              {currentPlan && (
                <div className="text-right">
                  <span className="text-lg font-bold text-white">
                    {formatPrice(currentPlan.pricing.monthly.price)}
                  </span>
                  <span className="text-gray-400 text-sm">/mo</span>
                </div>
              )}
            </div>
            {canManageBilling() ? (
              <a 
                href="/dashboard/billing" 
                className="w-full block text-center p-2.5 text-sm font-medium text-gray-300 hover:text-white bg-gray-800/50 hover:bg-gray-700/50 border border-gray-700/50 rounded-lg transition-all"
              >
                Manage Subscription
              </a>
            ) : (
              <a 
                href="/pricing" 
                className="w-full block text-center p-2.5 text-sm font-medium text-gray-300 hover:text-white bg-gray-800/50 hover:bg-gray-700/50 border border-gray-700/50 rounded-lg transition-all"
              >
                Upgrade Plan
              </a>
            )}
          </div>
        </div>

        {/* Email Preferences */}
        <div className="bg-gray-900/40 backdrop-blur-sm border border-gray-800/50 rounded-2xl p-6">
          <h3 className="text-lg font-bold text-white mb-4">Email Preferences</h3>
          <div className="space-y-4">
            <div className="flex items-center justify-between">
              <div>
                <span className="text-white text-sm font-medium">Weekly Progress</span>
                <p className="text-gray-400 text-xs">Get weekly writing summaries</p>
              </div>
              <label className="relative inline-flex items-center cursor-pointer">
                <input
                  type="checkbox"
                  checked={emailPreferences.weekly_progress_digest}
                  onChange={(e) => handleEmailPreferenceChange('weekly_progress_digest', e.target.checked)}
                  disabled={emailSaving}
                  className="sr-only peer"
                />
                <div className="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-teal-600"></div>
              </label>
            </div>

            <div className="flex items-center justify-between">
              <div>
                <span className="text-white text-sm font-medium">Product Updates</span>
                <p className="text-gray-400 text-xs">New features and improvements</p>
              </div>
              <label className="relative inline-flex items-center cursor-pointer">
                <input
                  type="checkbox"
                  checked={emailPreferences.product_updates}
                  onChange={(e) => handleEmailPreferenceChange('product_updates', e.target.checked)}
                  disabled={emailSaving}
                  className="sr-only peer"
                />
                <div className="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-teal-600"></div>
              </label>
            </div>

            <div className="flex items-center justify-between">
              <div>
                <span className="text-white text-sm font-medium">Account Notifications</span>
                <p className="text-gray-400 text-xs">Important account updates</p>
              </div>
              <label className="relative inline-flex items-center cursor-pointer">
                <input
                  type="checkbox"
                  checked={emailPreferences.account_notifications}
                  onChange={(e) => handleEmailPreferenceChange('account_notifications', e.target.checked)}
                  disabled={emailSaving}
                  className="sr-only peer"
                />
                <div className="w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-teal-600"></div>
              </label>
            </div>
          </div>
          
          {emailSaving && (
            <div className="mt-4 p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg">
              <p className="text-blue-300 text-sm flex items-center">
                <div className="animate-spin rounded-full h-3 w-3 border-2 border-blue-500 border-t-transparent mr-2"></div>
                Saving...
              </p>
            </div>
          )}
        </div>
      </div>

      {/* Profile Edit Dialog */}
      <ProfileEditDialog 
        isOpen={isEditDialogOpen}
        onClose={() => setIsEditDialogOpen(false)}
      />

      {/* Welcome Tour */}
      <WelcomeTour 
        isOpen={isTourOpen}
        onClose={() => closeTour('skipped')}
        onComplete={completeTour}
      />
    </DashboardLayout>
  )
}