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:
clawd
2026-02-18 20:12:45 +00:00
parent 5cce78f40f
commit e10e8f3fe2
3 changed files with 78 additions and 49 deletions

View File

@@ -2,6 +2,7 @@ import { Link } from 'react-router'
import { Heart, Clock } from 'lucide-react' import { Heart, Clock } from 'lucide-react'
import { useMutation, useQueryClient } from '@tanstack/react-query' import { useMutation, useQueryClient } from '@tanstack/react-query'
import { toggleFavorite } from '../../api/recipes' import { toggleFavorite } from '../../api/recipes'
import { useAuth } from '../../context/AuthContext'
import type { Recipe } from '../../api/types' import type { Recipe } from '../../api/types'
const gradients = [ const gradients = [
@@ -12,6 +13,7 @@ const gradients = [
] ]
export function RecipeCard({ recipe }: { recipe: Recipe }) { export function RecipeCard({ recipe }: { recipe: Recipe }) {
const { isAuthenticated } = useAuth()
const qc = useQueryClient() const qc = useQueryClient()
const favMutation = useMutation({ const favMutation = useMutation({
mutationFn: () => toggleFavorite(recipe.id), mutationFn: () => toggleFavorite(recipe.id),
@@ -36,22 +38,24 @@ export function RecipeCard({ recipe }: { recipe: Recipe }) {
<Link to={`/recipe/${recipe.slug}`}> <Link to={`/recipe/${recipe.slug}`}>
<h3 className="font-display text-base sm:text-lg text-espresso line-clamp-2">{recipe.title}</h3> <h3 className="font-display text-base sm:text-lg text-espresso line-clamp-2">{recipe.title}</h3>
</Link> </Link>
<div className="flex items-center justify-between mt-2"> {isAuthenticated && (
{totalTime > 0 && ( <div className="flex items-center justify-between mt-2">
<span className="flex items-center gap-1 text-warm-grey text-xs"> {totalTime > 0 && (
<Clock size={14} /> {totalTime} min <span className="flex items-center gap-1 text-warm-grey text-xs">
</span> <Clock size={14} /> {totalTime} min
)} </span>
<button )}
onClick={(e) => { e.preventDefault(); e.stopPropagation(); favMutation.mutate() }} <button
className="ml-auto p-2 -mr-2 min-w-[44px] min-h-[44px] flex items-center justify-center active:scale-125 transition-transform" onClick={(e) => { e.preventDefault(); e.stopPropagation(); favMutation.mutate() }}
> className="ml-auto p-2 -mr-2 min-w-[44px] min-h-[44px] flex items-center justify-center active:scale-125 transition-transform"
<Heart >
size={22} <Heart
className={recipe.is_favorite ? 'fill-primary text-primary' : 'text-warm-grey'} size={22}
/> className={recipe.is_favorite ? 'fill-primary text-primary' : 'text-warm-grey'}
</button> />
</div> </button>
</div>
)}
</div> </div>
</div> </div>
) )

View File

@@ -8,13 +8,14 @@ import { fetchRecipes, fetchRandomRecipe } from '../api/recipes'
import { fetchCategories } from '../api/categories' import { fetchCategories } from '../api/categories'
import { fetchTags } from '../api/tags' import { fetchTags } from '../api/tags'
import { RecipeCard } from '../components/recipe/RecipeCard' import { RecipeCard } from '../components/recipe/RecipeCard'
import { RecipeCardSmall } from '../components/recipe/RecipeCardSmall'
import type { Recipe } from '../api/types' import type { Recipe } from '../api/types'
import { Badge } from '../components/ui/Badge' import { Badge } from '../components/ui/Badge'
import { RecipeCardSkeleton } from '../components/ui/Skeleton' import { RecipeCardSkeleton } from '../components/ui/Skeleton'
import { EmptyState } from '../components/ui/EmptyState' import { EmptyState } from '../components/ui/EmptyState'
import { useAuth } from '../context/AuthContext'
export function HomePage() { export function HomePage() {
const { isAuthenticated, user } = useAuth()
const [activeCategory, setActiveCategory] = useState<string | undefined>() const [activeCategory, setActiveCategory] = useState<string | undefined>()
const [activeTag, setActiveTag] = useState<string | undefined>() const [activeTag, setActiveTag] = useState<string | undefined>()
const navigate = useNavigate() const navigate = useNavigate()
@@ -34,14 +35,8 @@ export function HomePage() {
queryFn: () => fetchRecipes({ category: activeCategory, sort: 'newest' }), queryFn: () => fetchRecipes({ category: activeCategory, sort: 'newest' }),
}) })
const { data: favoritesData } = useQuery({
queryKey: ['recipes', { favorite: true }],
queryFn: () => fetchRecipes({ favorite: true }),
})
const recipes = recipesData?.data ?? [] const recipes = recipesData?.data ?? []
const totalCount = recipesData?.total ?? 0 const totalCount = recipesData?.total ?? 0
const favorites = favoritesData?.data ?? []
// Filter by tag client-side if active // Filter by tag client-side if active
const filteredRecipes = activeTag const filteredRecipes = activeTag
@@ -69,11 +64,20 @@ export function HomePage() {
{/* TopBar */} {/* TopBar */}
<div className="flex items-center justify-between px-4 py-4"> <div className="flex items-center justify-between px-4 py-4">
<h1 className="font-display text-2xl text-espresso">🧁 Luna Recipes</h1> <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> </div>
{/* Recipe Counter */} {/* Recipe Counter (logged in only) */}
{totalCount > 0 && ( {isAuthenticated && totalCount > 0 && (
<motion.div <motion.div
className="px-4 pb-3" className="px-4 pb-3"
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -10 }}
@@ -85,8 +89,8 @@ export function HomePage() {
</motion.div> </motion.div>
)} )}
{/* Random Recipe Buttons */} {/* Random Recipe Buttons (logged in only) */}
{totalCount > 1 && ( {isAuthenticated && totalCount > 1 && (
<div className="px-4 pb-4 flex gap-2"> <div className="px-4 pb-4 flex gap-2">
<button <button
onClick={handleRandomCook} onClick={handleRandomCook}
@@ -105,26 +109,6 @@ export function HomePage() {
</div> </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 */} {/* Category Chips */}
{categories && (Array.isArray(categories) ? categories : []).length > 0 && ( {categories && (Array.isArray(categories) ? categories : []).length > 0 && (
<div className="flex gap-2 px-4 pb-2 overflow-x-auto scrollbar-hide"> <div className="flex gap-2 px-4 pb-2 overflow-x-auto scrollbar-hide">

View File

@@ -4,7 +4,7 @@ import { motion } from 'framer-motion'
import { Link } from 'react-router' import { Link } from 'react-router'
import { fetchRecipes } from '../api/recipes' import { fetchRecipes } from '../api/recipes'
import { fetchShopping } from '../api/shopping' 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 { useAuth } from '../context/AuthContext'
import { EmptyState } from '../components/ui/EmptyState' import { EmptyState } from '../components/ui/EmptyState'
import { Button } from '../components/ui/Button' import { Button } from '../components/ui/Button'
@@ -127,6 +127,47 @@ export function ProfilePage() {
<HouseholdCard /> <HouseholdCard />
</div> </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 */} {/* Stats */}
<div className="px-4 pb-4"> <div className="px-4 pb-4">
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">