Overview
The Favorites functionality allows authenticated users to save properties they’re interested in. Favorites are managed through a combination of the Properties API and Users API.
All favorites operations require authentication. Users can only manage their own favorites.
How Favorites Work
Favorites are stored in the user_favorites table, creating a many-to-many relationship between users and properties. The system provides three main operations:
Add to favorites - Add a property to your favorites
Remove from favorites - Remove a property from your favorites
Get favorites list - Retrieve all favorited property IDs
Add Property to Favorites
POST /properties/:id/favorite
const response = await api . properties . addToFavorites ( "123" );
Add a property to the authenticated user’s favorites list.
Endpoint
POST /api/properties/:id/favorite
Path Parameters
The property ID to add to favorites
Authentication
Requires valid session cookie. Returns 401 if not authenticated.
Response
Indicates if the property was added to favorites
Example Response
{
"success" : true ,
"message" : "Property added to favorites"
}
Usage Example
From src/contexts/AuthContext.tsx:336:
const toggleFavorite = async ( propertyId : string ) => {
if ( ! user ) {
toast . error ( "Debes iniciar sesión para guardar favoritos" );
return ;
}
const isFavorite = favoriteIds . includes ( propertyId );
if ( ! isFavorite ) {
await api . properties . addToFavorites ( propertyId );
setFavoriteIds (( prev ) => [ ... prev , propertyId ]);
toast . success ( "Agregado a favoritos" );
} else {
// Remove from favorites...
}
};
Error Responses
UNAUTHORIZED
NOT_FOUND
ALREADY_FAVORITED
{
"message" : "Authentication required" ,
"code" : "UNAUTHORIZED"
}
Remove Property from Favorites
DELETE /properties/:id/favorite
const response = await api . properties . removeFromFavorites ( "123" );
Remove a property from the authenticated user’s favorites list.
Endpoint
DELETE /api/properties/:id/favorite
Path Parameters
The property ID to remove from favorites
Authentication
Requires valid session cookie. Returns 401 if not authenticated.
Response
Indicates if the property was removed from favorites
Example Response
{
"success" : true ,
"message" : "Property removed from favorites"
}
Usage Example
From src/contexts/AuthContext.tsx:349:
const toggleFavorite = async ( propertyId : string ) => {
if ( ! user ) {
toast . error ( "Debes iniciar sesión para guardar favoritos" );
return ;
}
const isFavorite = favoriteIds . includes ( propertyId );
if ( isFavorite ) {
await api . properties . removeFromFavorites ( propertyId );
setFavoriteIds (( prev ) => prev . filter (( id ) => id !== propertyId ));
toast . success ( "Eliminado de favoritos" );
} else {
// Add to favorites...
}
};
Get User’s Favorites
const response = await api . users . getFavorites ();
Retrieve all property IDs favorited by the authenticated user.
Endpoint
GET /api/users/favorites
Authentication
Requires valid session cookie. Returns 401 if not authenticated.
Response
Indicates if the request was successful
Example Response
{
"success" : true ,
"data" : [ "123" , "456" , "789" ]
}
Usage Example
From src/contexts/AuthContext.tsx:66:
useEffect (() => {
const loadFavorites = async () => {
if ( user ) {
try {
const favoritesResponse = await api . users . getFavorites ();
setFavoriteIds ( favoritesResponse . data || []);
} catch ( error ) {
console . error ( "Error loading favorites:" , error );
}
}
};
loadFavorites ();
}, [ user ]);
Complete Favorites Implementation
Full Context Example
Here’s a complete implementation of favorites management from the codebase:
const AuthContext = createContext < AuthContextType | undefined >( undefined );
export const AuthProvider = ({ children } : { children : React . ReactNode }) => {
const [ user , setUser ] = useState < User | null >( null );
const [ favoriteIds , setFavoriteIds ] = useState < string []>([]);
// Load favorites when user logs in
useEffect (() => {
const loadFavorites = async () => {
if ( user ) {
try {
const favoritesResponse = await api . users . getFavorites ();
setFavoriteIds ( favoritesResponse . data || []);
} catch ( error ) {
console . error ( "Error loading favorites:" , error );
}
} else {
setFavoriteIds ([]);
}
};
loadFavorites ();
}, [ user ]);
// Toggle favorite status
const toggleFavorite = async ( propertyId : string ) => {
if ( ! user ) {
toast . error ( "Debes iniciar sesión para guardar favoritos" );
return ;
}
const isFavorite = favoriteIds . includes ( propertyId );
try {
if ( isFavorite ) {
await api . properties . removeFromFavorites ( propertyId );
setFavoriteIds (( prev ) => prev . filter (( id ) => id !== propertyId ));
toast . success ( "Eliminado de favoritos" );
} else {
await api . properties . addToFavorites ( propertyId );
setFavoriteIds (( prev ) => [ ... prev , propertyId ]);
toast . success ( "Agregado a favoritos" );
}
} catch ( error ) {
toast . error ( "Error al actualizar favoritos" );
console . error ( "Favorite toggle error:" , error );
}
};
// Check if property is favorited
const isFavorite = ( propertyId : string ) => {
return favoriteIds . includes ( propertyId );
};
return (
< AuthContext . Provider
value = {{
user ,
favoriteIds ,
toggleFavorite ,
isFavorite ,
}}
>
{ children }
</ AuthContext . Provider >
);
};
Displaying Favorites Page
From src/pages/FavoritesPage.tsx:28:
const FavoritesPage = () => {
const [ properties , setProperties ] = useState < Property []>([]);
const [ loading , setLoading ] = useState ( true );
useEffect (() => {
const loadFavorites = async () => {
try {
// Get favorite property IDs
const favoritesResponse = await api . users . getFavorites ();
const favoriteIds = favoritesResponse . data || [];
if ( favoriteIds . length === 0 ) {
setProperties ([]);
setLoading ( false );
return ;
}
// Fetch full property details for each favorite
const propertiesData = await Promise . all (
favoriteIds . map (( id ) =>
api . properties . get ( String ( id )). catch (() => {
console . error ( `Failed to fetch property ${ id } ` );
return null ;
})
)
);
// Filter out failed requests
const validProperties = propertiesData
. filter (( p ) => p !== null )
. map (( p ) => p . data );
setProperties ( validProperties );
} catch ( error ) {
console . error ( "Error loading favorites:" , error );
toast . error ( "Error al cargar favoritos" );
} finally {
setLoading ( false );
}
};
loadFavorites ();
}, []);
if ( loading ) return < LoadingSpinner />;
if ( properties . length === 0 ) return < EmptyState />;
return (
< div >
{ properties . map (( property ) => (
< PropertyCard key = {property. id } property = { property } />
))}
</ div >
);
};
UI Integration
interface FavoriteButtonProps {
propertyId : string ;
className ?: string ;
}
const FavoriteButton = ({ propertyId , className } : FavoriteButtonProps ) => {
const { isFavorite , toggleFavorite , user } = useAuth ();
const favorite = isFavorite ( propertyId );
const handleClick = ( e : React . MouseEvent ) => {
e . preventDefault ();
e . stopPropagation ();
toggleFavorite ( propertyId );
};
return (
< button
onClick = { handleClick }
className = { `favorite-btn ${ favorite ? 'active' : '' } ${ className } ` }
aria - label = {favorite ? 'Remove from favorites' : 'Add to favorites' }
>
{ favorite ? < HeartFilledIcon /> : < HeartOutlineIcon />}
</ button >
);
};
Database Schema
The favorites functionality uses the user_favorites table:
CREATE TABLE user_favorites (
id SERIAL PRIMARY KEY ,
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE ,
property_id INTEGER REFERENCES properties(id) ON DELETE CASCADE ,
created_at TIMESTAMP DEFAULT NOW (),
UNIQUE (user_id, property_id)
);
Key Features:
Cascade deletion: If a user or property is deleted, favorites are automatically removed
Unique constraint: Prevents duplicate favorites
Indexed for fast lookup
Best Practices
Optimistic UI Updates
Update the UI immediately before the API call completes:
const toggleFavorite = async ( propertyId : string ) => {
// Update UI immediately
const wasFavorite = isFavorite ( propertyId );
if ( wasFavorite ) {
setFavoriteIds (( prev ) => prev . filter (( id ) => id !== propertyId ));
} else {
setFavoriteIds (( prev ) => [ ... prev , propertyId ]);
}
try {
// Make API call
if ( wasFavorite ) {
await api . properties . removeFromFavorites ( propertyId );
} else {
await api . properties . addToFavorites ( propertyId );
}
} catch ( error ) {
// Revert on error
if ( wasFavorite ) {
setFavoriteIds (( prev ) => [ ... prev , propertyId ]);
} else {
setFavoriteIds (( prev ) => prev . filter (( id ) => id !== propertyId ));
}
toast . error ( "Error updating favorites" );
}
};
Handle Unauthenticated Users
Always check authentication before allowing favorite operations:
const toggleFavorite = async ( propertyId : string ) => {
if ( ! user ) {
toast . error ( "Debes iniciar sesión para guardar favoritos" );
navigate ( '/auth' );
return ;
}
// Continue with favorite operation...
};
Cache Favorites Locally
Load favorites once at login and maintain in memory:
useEffect (() => {
if ( user ) {
loadFavorites ();
} else {
setFavoriteIds ([]);
}
}, [ user ]);