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 { 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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user