bookwiz.io / lib / hooks / useProfile.ts
useProfile.ts
Raw
import { useState, useEffect, useRef } from 'react'
import { useAuth } from '@/components/AuthProvider'
import { supabase } from '@/lib/supabase'

export interface UserProfile {
  id: string
  email: string | null
  full_name: string | null
  avatar_url: string | null
  created_at: string
  updated_at: string
  welcome_tour_completed?: boolean
  welcome_tour_completed_at?: string | null
  welcome_tour_started_at?: string | null
  welcome_tour_current_step?: number
  welcome_tour_steps_viewed?: number[]
  welcome_tour_completion_type?: 'completed' | 'skipped' | 'abandoned' | null
  welcome_tour_selected_plan?: string | null
  welcome_tour_selected_billing?: 'month' | 'year' | null
  welcome_tour_total_time_seconds?: number
  welcome_tour_video_watched?: boolean
  welcome_tour_version?: string
}

// Cache profile data across component re-renders
let profileCache: { userId: string; data: UserProfile | null; timestamp: number } | null = null;
let ongoingRequest: { userId: string; promise: Promise<UserProfile | null> } | null = null;
const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes

export function useProfile() {
  const { user } = useAuth()
  const [profile, setProfile] = useState<UserProfile | null>(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState<string | null>(null)
  const hasFetched = useRef(false)

  useEffect(() => {
    if (!user?.id) {
      setProfile(null)
      setLoading(false)
      hasFetched.current = false
      return
    }

    // Check cache first
    const now = Date.now()
    if (profileCache && 
        profileCache.userId === user.id && 
        (now - profileCache.timestamp) < CACHE_DURATION) {
      setProfile(profileCache.data)
      setLoading(false)
      hasFetched.current = true
      return
    }

    // Only fetch if we haven't fetched for this user yet
    if (!hasFetched.current) {
      fetchProfile()
    }
  }, [user?.id])

  const fetchProfile = async (): Promise<UserProfile | null> => {
    if (!user?.id) return null

    // Check if there's already an ongoing request for this user
    if (ongoingRequest && ongoingRequest.userId === user.id) {
      try {
        const result = await ongoingRequest.promise
        if (result) {
          setProfile(result)
        }
        hasFetched.current = true
        setLoading(false)
        return result
      } catch (err) {
        setError(err instanceof Error ? err.message : 'Failed to fetch profile')
        setLoading(false)
        return null
      }
    }

    // Create new request promise
    const requestPromise = (async (): Promise<UserProfile | null> => {
      const { data, error } = await supabase
        .from('profiles')
        .select('*')
        .eq('id', user.id)
        .single()

      if (error) {
        if (error.code === 'PGRST116') {
          // No profile found, create one from auth metadata
          const profileData = {
            id: user.id,
            email: user.email,
            full_name: user.user_metadata?.full_name || user.user_metadata?.name || '',
            avatar_url: user.user_metadata?.avatar_url || user.user_metadata?.picture || null,
          }

          const { data: newProfile, error: createError } = await supabase
            .from('profiles')
            .insert(profileData)
            .select()
            .single()

          if (createError) throw createError
          return newProfile
        } else {
          throw error
        }
      }
      return data
    })()

    // Store the ongoing request
    ongoingRequest = {
      userId: user.id,
      promise: requestPromise
    }

    try {
      setLoading(true)
      setError(null)

      const result = await requestPromise
      
      // Update cache
      profileCache = {
        userId: user.id,
        data: result,
        timestamp: Date.now()
      }
      
      setProfile(result)
      hasFetched.current = true
      return result
    } catch (err) {
      console.error('Error fetching profile:', err)
      setError(err instanceof Error ? err.message : 'Failed to fetch profile')
      return null
    } finally {
      setLoading(false)
      // Clear ongoing request
      ongoingRequest = null
    }
  }



  const updateProfile = async (updates: Partial<Pick<UserProfile, 'full_name' | 'avatar_url'>>) => {
    if (!user?.id || !profile) return

    try {
      const { data, error } = await supabase
        .from('profiles')
        .update({
          ...updates,
          updated_at: new Date().toISOString()
        })
        .eq('id', user.id)
        .select()
        .single()

      if (error) throw error
      
      // Update cache
      profileCache = {
        userId: user.id,
        data: data,
        timestamp: Date.now()
      }
      setProfile(data)
      return data
    } catch (err) {
      console.error('Error updating profile:', err)
      throw err
    }
  }

  // Force refresh and clear cache - now returns a Promise
  const refetch = async (): Promise<UserProfile | null> => {
    if (!user?.id) return null
    
    // Clear cache and force fresh fetch
    profileCache = null
    hasFetched.current = false
    ongoingRequest = null
    
    // Fetch fresh profile data
    const result = await fetchProfile()
    return result
  }

  return {
    profile,
    loading,
    error,
    updateProfile,
    refetch
  }
}