All files / src/hooks useRecipeDetail.ts

80.43% Statements 37/46
66.66% Branches 12/18
66.66% Functions 6/9
85.36% Lines 35/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90                        30x 30x 30x 30x 30x   30x 30x 30x 30x 30x   30x     30x             30x 11x 11x 11x 11x 11x 9x 9x 9x 9x     1x 1x 1x     10x     11x       30x   30x         30x             30x 30x   30x           1x     1x          
import { useState, useEffect } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import { toast } from 'sonner'
import { api, type RecipeDetail as RecipeDetailType } from '../api/client'
import { useProfile } from '../contexts/ProfileContext'
import { useAIStatus } from '../contexts/AIStatusContext'
import { useTipsPolling } from './useTipsPolling'
import { useServingScale } from './useServingScale'
 
type Tab = 'ingredients' | 'instructions' | 'nutrition' | 'tips'
 
export function useRecipeDetail() {
  const navigate = useNavigate()
  const { id } = useParams<{ id: string }>()
  const recipeId = Number(id)
  const { profile, isFavorite, toggleFavorite } = useProfile()
  const aiStatus = useAIStatus()
 
  const [recipe, setRecipe] = useState<RecipeDetailType | null>(null)
  const [loading, setLoading] = useState(true)
  const [activeTab, setActiveTab] = useState<Tab>('ingredients')
  const [metaExpanded, setMetaExpanded] = useState(true)
  const [showRemixModal, setShowRemixModal] = useState(false)
 
  const { servings, scaledData, scalingLoading, setServings, setScaledData, handleServingChange } =
    useServingScale(recipe, profile?.id)
 
  const { tips, tipsLoading, tipsPolling, handleGenerateTips } = useTipsPolling({
    recipe,
    aiAvailable: aiStatus.available,
    activeTab,
    setRecipe,
  })
 
  useEffect(() => {
    Iif (!recipeId) return
    let cancelled = false
    ;(async () => {
      try {
        const recipeData = await api.recipes.get(recipeId)
        Eif (!cancelled) {
          setRecipe(recipeData)
          setServings(recipeData.servings)
          setScaledData(null)
        }
      } catch (error) {
        Eif (!cancelled) {
          console.error('Failed to load recipe:', error)
          toast.error('Failed to load recipe')
        }
      } finally {
        Eif (!cancelled) setLoading(false)
      }
    })()
    return () => { cancelled = true }
    // eslint-disable-next-line react-hooks/exhaustive-deps -- only re-run when recipeId changes
  }, [recipeId])
 
  const canShowServingAdjustment = aiStatus.available && recipe?.servings !== null
 
  const handleFavoriteToggle = async () => {
    if (!recipe) return
    await toggleFavorite(recipe)
  }
 
  const handleRemixCreated = async (newRecipeId: number) => {
    try { await api.history.record(newRecipeId) } catch (error) {
      console.error('Failed to record history:', error)
    }
    navigate(`/recipe/${newRecipeId}`)
  }
 
  const recipeIsFavorite = recipe ? isFavorite(recipe.id) : false
  const imageUrl = recipe ? (recipe.image || recipe.image_url) : null
 
  return {
    recipe, loading, activeTab, setActiveTab, metaExpanded, setMetaExpanded,
    servings, showRemixModal, setShowRemixModal, scaledData, scalingLoading,
    tips, tipsLoading, tipsPolling, profile, aiStatus, recipeId,
    canShowServingAdjustment, recipeIsFavorite, imageUrl,
    handleServingChange, handleGenerateTips, handleFavoriteToggle,
    handleStartCooking: () => navigate(`/recipe/${recipeId}/play`),
    handleAddToNewCollection: () => navigate(`/collections?addRecipe=${recipeId}`),
    handleRemixCreated,
    handleBack: () => navigate(-1),
  }
}
 
export type { Tab }
 
← Back to Dashboard