All files / src/screens AllRecipes.tsx

65.71% Statements 23/35
50% Branches 8/16
66.66% Functions 8/12
61.29% Lines 19/31

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 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139                      14x 14x 14x 14x   14x 7x 7x 7x 7x 7x             7x     7x     14x                 14x 14x                 14x                                                                           2x                     3x                                 5x                                      
import { useState, useEffect, useMemo } from 'react'
import { useNavigate } from 'react-router-dom'
import { BookOpen, Search } from 'lucide-react'
import { toast } from 'sonner'
import { api, type Recipe } from '../api/client'
import NavHeader from '../components/NavHeader'
import RecipeCard from '../components/RecipeCard'
import RecipeSearchFilter from '../components/RecipeSearchFilter'
import { RecipeGridSkeleton } from '../components/Skeletons'
 
export default function AllRecipes() {
  const navigate = useNavigate()
  const [recipes, setRecipes] = useState<Recipe[]>([])
  const [loading, setLoading] = useState(true)
  const [searchQuery, setSearchQuery] = useState('')
 
  useEffect(() => {
    let cancelled = false
    ;(async () => {
      try {
        const data = await api.recipes.list(1000)
        Eif (!cancelled) setRecipes(data)
      } catch (error) {
        if (!cancelled) {
          console.error('Failed to load recipes:', error)
          toast.error('Failed to load recipes')
        }
      } finally {
        Eif (!cancelled) setLoading(false)
      }
    })()
    return () => { cancelled = true }
  }, [])
 
  const handleRecipeClick = async (recipeId: number) => {
    try {
      await api.history.record(recipeId)
    } catch (error) {
      console.error('Failed to record history:', error)
    }
    navigate(`/recipe/${recipeId}`)
  }
 
  const filteredRecipes = useMemo(() => {
    Eif (!searchQuery.trim()) return recipes
    const query = searchQuery.toLowerCase()
    return recipes.filter(
      (recipe) =>
        recipe.title.toLowerCase().includes(query) ||
        recipe.host.toLowerCase().includes(query)
    )
  }, [recipes, searchQuery])
 
  return (
    <div className="min-h-screen bg-background">
      <NavHeader />
      <main className="px-4 py-6">
        <div className="mx-auto max-w-4xl">
          <h2 className="mb-4 text-lg font-medium text-foreground">My Recipes</h2>
          {loading ? (
            <RecipeGridSkeleton count={8} />
          ) : recipes.length > 0 ? (
            <RecipeList
              recipes={recipes}
              filteredRecipes={filteredRecipes}
              searchQuery={searchQuery}
              onSearchChange={setSearchQuery}
              onRecipeClick={handleRecipeClick}
            />
          ) : (
            <EmptyRecipes onImport={() => navigate('/home')} />
          )}
        </div>
      </main>
    </div>
  )
}
 
function RecipeList({
  recipes,
  filteredRecipes,
  searchQuery,
  onSearchChange,
  onRecipeClick,
}: {
  recipes: Recipe[]
  filteredRecipes: Recipe[]
  searchQuery: string
  onSearchChange: (q: string) => void
  onRecipeClick: (id: number) => void
}) {
  return (
    <>
      <RecipeSearchFilter
        searchQuery={searchQuery}
        onSearchChange={onSearchChange}
        totalCount={recipes.length}
        filteredCount={filteredRecipes.length}
      />
      {filteredRecipes.length > 0 ? (
        <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-4">
          {filteredRecipes.map((recipe) => (
            <RecipeCard
              key={recipe.id}
              recipe={recipe}
              onClick={() => onRecipeClick(recipe.id)}
            />
          ))}
        </div>
      ) : (
        <div className="py-8 text-center text-muted-foreground">
          No recipes match "{searchQuery}"
        </div>
      )}
    </>
  )
}
 
function EmptyRecipes({ onImport }: { onImport: () => void }) {
  return (
    <div className="flex flex-col items-center justify-center rounded-xl border-2 border-dashed border-border py-12">
      <div className="mb-4 rounded-full bg-muted p-4">
        <BookOpen className="h-8 w-8 text-muted-foreground" />
      </div>
      <h3 className="mb-2 text-lg font-medium text-foreground">No recipes yet</h3>
      <p className="mb-4 text-center text-muted-foreground">
        Import recipes from the web to see them here
      </p>
      <button
        onClick={onImport}
        className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-primary-foreground transition-colors hover:bg-primary/90"
      >
        <Search className="h-4 w-4" />
        Import Recipes
      </button>
    </div>
  )
}
 
← Back to Dashboard