feat: public/private view separation + profile favorites
- Hide time, favorites heart, recipe counter, random buttons for guests - Move favorites section from HomePage to ProfilePage (personal) - Make avatar button clickable → login (guest) / profile (logged in) - Show user avatar in top bar when available - Add Airfryer category
This commit is contained in:
@@ -2,6 +2,7 @@ import { Link } from 'react-router'
|
||||
import { Heart, Clock } from 'lucide-react'
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { toggleFavorite } from '../../api/recipes'
|
||||
import { useAuth } from '../../context/AuthContext'
|
||||
import type { Recipe } from '../../api/types'
|
||||
|
||||
const gradients = [
|
||||
@@ -12,6 +13,7 @@ const gradients = [
|
||||
]
|
||||
|
||||
export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
||||
const { isAuthenticated } = useAuth()
|
||||
const qc = useQueryClient()
|
||||
const favMutation = useMutation({
|
||||
mutationFn: () => toggleFavorite(recipe.id),
|
||||
@@ -36,6 +38,7 @@ export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
||||
<Link to={`/recipe/${recipe.slug}`}>
|
||||
<h3 className="font-display text-base sm:text-lg text-espresso line-clamp-2">{recipe.title}</h3>
|
||||
</Link>
|
||||
{isAuthenticated && (
|
||||
<div className="flex items-center justify-between mt-2">
|
||||
{totalTime > 0 && (
|
||||
<span className="flex items-center gap-1 text-warm-grey text-xs">
|
||||
@@ -52,6 +55,7 @@ export function RecipeCard({ recipe }: { recipe: Recipe }) {
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -8,13 +8,14 @@ import { fetchRecipes, fetchRandomRecipe } from '../api/recipes'
|
||||
import { fetchCategories } from '../api/categories'
|
||||
import { fetchTags } from '../api/tags'
|
||||
import { RecipeCard } from '../components/recipe/RecipeCard'
|
||||
import { RecipeCardSmall } from '../components/recipe/RecipeCardSmall'
|
||||
import type { Recipe } from '../api/types'
|
||||
import { Badge } from '../components/ui/Badge'
|
||||
import { RecipeCardSkeleton } from '../components/ui/Skeleton'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
export function HomePage() {
|
||||
const { isAuthenticated, user } = useAuth()
|
||||
const [activeCategory, setActiveCategory] = useState<string | undefined>()
|
||||
const [activeTag, setActiveTag] = useState<string | undefined>()
|
||||
const navigate = useNavigate()
|
||||
@@ -34,14 +35,8 @@ export function HomePage() {
|
||||
queryFn: () => fetchRecipes({ category: activeCategory, sort: 'newest' }),
|
||||
})
|
||||
|
||||
const { data: favoritesData } = useQuery({
|
||||
queryKey: ['recipes', { favorite: true }],
|
||||
queryFn: () => fetchRecipes({ favorite: true }),
|
||||
})
|
||||
|
||||
const recipes = recipesData?.data ?? []
|
||||
const totalCount = recipesData?.total ?? 0
|
||||
const favorites = favoritesData?.data ?? []
|
||||
|
||||
// Filter by tag client-side if active
|
||||
const filteredRecipes = activeTag
|
||||
@@ -69,11 +64,20 @@ export function HomePage() {
|
||||
{/* TopBar */}
|
||||
<div className="flex items-center justify-between px-4 py-4">
|
||||
<h1 className="font-display text-2xl text-espresso">🧁 Luna Recipes</h1>
|
||||
<div className="w-9 h-9 rounded-full bg-primary-light flex items-center justify-center text-sm">👤</div>
|
||||
<button
|
||||
onClick={() => navigate(isAuthenticated ? '/profile' : '/login')}
|
||||
className="w-9 h-9 rounded-full bg-primary-light flex items-center justify-center text-sm overflow-hidden"
|
||||
>
|
||||
{isAuthenticated && user?.avatar_url ? (
|
||||
<img src={user.avatar_url} alt="" className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<span>👤</span>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Recipe Counter */}
|
||||
{totalCount > 0 && (
|
||||
{/* Recipe Counter (logged in only) */}
|
||||
{isAuthenticated && totalCount > 0 && (
|
||||
<motion.div
|
||||
className="px-4 pb-3"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
@@ -85,8 +89,8 @@ export function HomePage() {
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Random Recipe Buttons */}
|
||||
{totalCount > 1 && (
|
||||
{/* Random Recipe Buttons (logged in only) */}
|
||||
{isAuthenticated && totalCount > 1 && (
|
||||
<div className="px-4 pb-4 flex gap-2">
|
||||
<button
|
||||
onClick={handleRandomCook}
|
||||
@@ -105,26 +109,6 @@ export function HomePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Favorites Section */}
|
||||
{favorites.length > 0 && (
|
||||
<div className="pb-4">
|
||||
<h2 className="font-display text-lg text-espresso px-4 mb-2">❤️ Favoriten</h2>
|
||||
<div className="flex gap-3 px-4 overflow-x-auto scrollbar-hide pb-1">
|
||||
{favorites.map((recipe, i) => (
|
||||
<motion.div
|
||||
key={recipe.id}
|
||||
className="min-w-[260px] max-w-[260px]"
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: i * 0.05 }}
|
||||
>
|
||||
<RecipeCardSmall recipe={recipe} />
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Category Chips */}
|
||||
{categories && (Array.isArray(categories) ? categories : []).length > 0 && (
|
||||
<div className="flex gap-2 px-4 pb-2 overflow-x-auto scrollbar-hide">
|
||||
|
||||
@@ -4,7 +4,7 @@ import { motion } from 'framer-motion'
|
||||
import { Link } from 'react-router'
|
||||
import { fetchRecipes } from '../api/recipes'
|
||||
import { fetchShopping } from '../api/shopping'
|
||||
import { LogOut, Info, Heart, BookOpen, ShoppingCart, Settings, User, ChevronRight } from 'lucide-react'
|
||||
import { LogOut, Info, Heart, BookOpen, ShoppingCart, Settings, User, ChevronRight, Clock } from 'lucide-react'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { EmptyState } from '../components/ui/EmptyState'
|
||||
import { Button } from '../components/ui/Button'
|
||||
@@ -127,6 +127,47 @@ export function ProfilePage() {
|
||||
<HouseholdCard />
|
||||
</div>
|
||||
|
||||
{/* Favoriten */}
|
||||
{(favRecipes?.data ?? []).length > 0 && (
|
||||
<div className="px-4 pb-4">
|
||||
<div className="bg-surface rounded-2xl p-4 shadow-sm">
|
||||
<div className="flex items-center gap-2 text-espresso font-medium text-sm mb-3">
|
||||
<Heart size={16} className="text-primary" />
|
||||
Meine Favoriten
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{(favRecipes?.data ?? []).map((recipe) => {
|
||||
const totalTime = recipe.total_time_min || ((recipe.prep_time_min || 0) + (recipe.cook_time_min || 0))
|
||||
return (
|
||||
<Link
|
||||
key={recipe.id}
|
||||
to={`/recipe/${recipe.slug}`}
|
||||
className="flex items-center gap-3 p-2 rounded-xl hover:bg-sand/30 transition-colors"
|
||||
>
|
||||
{recipe.image_url ? (
|
||||
<img src={recipe.image_url} alt={recipe.title} className="w-12 h-12 rounded-xl object-cover flex-shrink-0" />
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-xl bg-primary-light flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-lg">🍰</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-sm font-medium text-espresso truncate">{recipe.title}</h3>
|
||||
{totalTime > 0 && (
|
||||
<span className="flex items-center gap-1 text-xs text-warm-grey mt-0.5">
|
||||
<Clock size={11} /> {totalTime} min
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<ChevronRight size={16} className="text-warm-grey flex-shrink-0" />
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats */}
|
||||
<div className="px-4 pb-4">
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
|
||||
Reference in New Issue
Block a user