Favorites API
Endpoints for managing user favorites (saved listings).
All favorites endpoints require authentication.
Toggle Favorite
Add or remove a listing from favorites.
POST /api/favorites/toggleRequest Body
{
"listingId": "clx123abc..."
}Response
{
"data": {
"favorited": true,
"listing": {
"id": "clx123abc...",
"title": "Luxury Beach Villa"
}
}
}If already favorited, removes it:
{
"data": {
"favorited": false,
"listing": {
"id": "clx123abc...",
"title": "Luxury Beach Villa"
}
}
}Get User Favorites
List all favorited listings for the authenticated user.
GET /api/favoritesQuery Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 20 | Items per page |
Response
{
"data": [
{
"listingId": "clx123abc...",
"createdAt": "2024-02-15T10:30:00Z",
"listing": {
"id": "clx123abc...",
"title": "Luxury Beach Villa",
"slug": "luxury-beach-villa-accra",
"region": "Greater Accra",
"city": "Accra",
"priceGHS": "850.00",
"bedrooms": 3,
"maxGuests": 6,
"images": [
{
"url": "https://res.cloudinary.com/...",
"isPrimary": true
}
]
}
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 5,
"totalPages": 1,
"hasMore": false
}
}Check if Favorited
Check if a specific listing is favorited.
GET /api/favorites/check/{listingId}Response
{
"data": {
"favorited": true
}
}Server Action Implementation
// lib/actions/favorites.ts
"use server"
import { createClient } from '@/lib/supabase/server'
import { prisma } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function toggleFavorite(listingId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
throw new Error('Please log in to save favorites')
}
// Check if already favorited
const existing = await prisma.favorite.findUnique({
where: {
userId_listingId: {
userId: user.id,
listingId
}
}
})
if (existing) {
// Remove favorite
await prisma.favorite.delete({
where: {
userId_listingId: {
userId: user.id,
listingId
}
}
})
revalidatePath('/dashboard/favorites')
return { favorited: false }
} else {
// Add favorite
await prisma.favorite.create({
data: {
userId: user.id,
listingId
}
})
revalidatePath('/dashboard/favorites')
return { favorited: true }
}
}
export async function getUserFavorites() {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) {
return []
}
return prisma.favorite.findMany({
where: { userId: user.id },
include: {
listing: {
include: {
images: {
where: { isPrimary: true },
take: 1
}
}
}
},
orderBy: { createdAt: 'desc' }
})
}
export async function isFavorited(listingId: string) {
const supabase = await createClient()
const { data: { user } } = await supabase.auth.getUser()
if (!user) return false
const favorite = await prisma.favorite.findUnique({
where: {
userId_listingId: {
userId: user.id,
listingId
}
}
})
return !!favorite
}Component Usage
Favorite Button
// components/features/favorites/favorite-button.tsx
"use client"
import { useState, useTransition } from 'react'
import { Heart } from 'lucide-react'
import { toggleFavorite } from '@/lib/actions/favorites'
interface FavoriteButtonProps {
listingId: string
initialFavorited: boolean
}
export function FavoriteButton({ listingId, initialFavorited }: FavoriteButtonProps) {
const [favorited, setFavorited] = useState(initialFavorited)
const [isPending, startTransition] = useTransition()
function handleClick() {
startTransition(async () => {
const result = await toggleFavorite(listingId)
setFavorited(result.favorited)
})
}
return (
<button
onClick={handleClick}
disabled={isPending}
className="p-2 rounded-full bg-white/90 backdrop-blur-sm hover:bg-white transition-colors"
>
<Heart
className={`w-5 h-5 ${
favorited ? 'fill-red-500 text-red-500' : 'text-gray-600'
}`}
/>
</button>
)
}With Auth Check
// components/features/favorites/favorite-button-wrapper.tsx
import { FavoriteButton } from './favorite-button'
import { isFavorited } from '@/lib/actions/favorites'
import { getCurrentUser } from '@/lib/auth'
export async function FavoriteButtonWrapper({ listingId }: { listingId: string }) {
const user = await getCurrentUser()
if (!user) {
// Show login prompt instead
return <FavoriteButtonLogin />
}
const favorited = await isFavorited(listingId)
return <FavoriteButton listingId={listingId} initialFavorited={favorited} />
}