L14: Add to PropertyCard
Integrate the favorite button into PropertyCard and ListingDetailsCard
Let's add the favorite button to our listing cards so users can favorite listings!
Current PropertyCard Structure
Our PropertyCard component displays listing information:
function PropertyCard({ listing }) {
return (
<div className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{listing.title}</h3>
<p className="text-gray-600 mb-2">{listing.description}</p>
<p className="text-lg font-bold">${listing.price}/night</p>
</div>
</div>
);
}We need to add the favorite button in the top-right corner of the image.
Step 1: Import the Button
Add the import at the top:
import ListingFavoriteButton from './ListingFavoriteButton';
function PropertyCard({ listing }) {
return (
<div className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{listing.title}</h3>
<p className="text-gray-600 mb-2">{listing.description}</p>
<p className="text-lg font-bold">${listing.price}/night</p>
</div>
</div>
);
}Step 2: Add Relative Positioning
Wrap the image in a container with relative positioning:
import ListingFavoriteButton from './ListingFavoriteButton';
function PropertyCard({ listing }) {
return (
<div className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
<div className="relative">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-48 object-cover"
/>
</div>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{listing.title}</h3>
<p className="text-gray-600 mb-2">{listing.description}</p>
<p className="text-lg font-bold">${listing.price}/night</p>
</div>
</div>
);
}Why relative? So the button can be positioned absolutely inside it!
Step 3: Add the Button
Add the button inside the relative container:
import ListingFavoriteButton from './ListingFavoriteButton';
function PropertyCard({ listing }) {
return (
<div className="border rounded-lg overflow-hidden hover:shadow-lg transition-shadow">
<div className="relative">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<div className="absolute top-2 right-2">
<ListingFavoriteButton listingId={listing.id} />
</div>
</div>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{listing.title}</h3>
<p className="text-gray-600 mb-2">{listing.description}</p>
<p className="text-lg font-bold">${listing.price}/night</p>
</div>
</div>
);
}That's it! The button now appears in the top-right corner! 🎉
Complete PropertyCard Code
Here's the complete updated component:
import { Link } from 'react-router-dom';
import ListingFavoriteButton from './ListingFavoriteButton';
function PropertyCard({ listing }) {
return (
<Link
to={`/listings/${listing.id}`}
className="block border rounded-lg overflow-hidden hover:shadow-lg transition-shadow"
>
<div className="relative">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<div className="absolute top-2 right-2">
<ListingFavoriteButton listingId={listing.id} />
</div>
</div>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{listing.title}</h3>
<p className="text-gray-600 text-sm mb-2 line-clamp-2">
{listing.description}
</p>
<p className="text-lg font-bold">${listing.price}/night</p>
</div>
</Link>
);
}
export default PropertyCard;Understanding the Positioning
Let's break down how the positioning works:
Add to ListingDetailsCard
Let's also add the button to the details page:
import ListingFavoriteButton from './ListingFavoriteButton';
function ListingDetailsCard({ listing }) {
return (
<div className="max-w-4xl mx-auto p-6">
<div className="relative mb-6">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-96 object-cover rounded-lg"
/>
<div className="absolute top-4 right-4">
<ListingFavoriteButton listingId={listing.id} />
</div>
</div>
<div className="mb-6">
<h1 className="text-3xl font-bold mb-2">{listing.title}</h1>
<p className="text-gray-600 mb-4">{listing.description}</p>
<p className="text-2xl font-bold">${listing.price}/night</p>
</div>
{/* More details... */}
</div>
);
}
export default ListingDetailsCard;Same pattern:
- Wrap image in relative container
- Add button with absolute positioning
- Position in top-right corner
Styling Variations
Standard placement (what we have):
<div className="absolute top-2 right-2">
<ListingFavoriteButton listingId={listing.id} />
</div>Clean and simple!
Add background for better visibility:
<div className="absolute top-2 right-2 bg-white/80 backdrop-blur-sm rounded-full">
<ListingFavoriteButton listingId={listing.id} />
</div>Classes explained:
bg-white/80- 80% opaque white backgroundbackdrop-blur-sm- Blur content behindrounded-full- Circular background
Better for images with light colors!
Alternative position:
<div className="absolute bottom-2 right-2">
<ListingFavoriteButton listingId={listing.id} />
</div>Positioned at bottom instead of top!
Add shadow for depth:
<div className="absolute top-2 right-2 shadow-lg rounded-full">
<ListingFavoriteButton listingId={listing.id} />
</div>Shadow makes button stand out more!
Enhanced Version with Background
For better visibility on all images:
import { Link } from 'react-router-dom';
import ListingFavoriteButton from './ListingFavoriteButton';
function PropertyCard({ listing }) {
return (
<Link
to={`/listings/${listing.id}`}
className="block border rounded-lg overflow-hidden hover:shadow-lg transition-shadow"
>
<div className="relative">
<img
src={listing.images[0]}
alt={listing.title}
className="w-full h-48 object-cover"
/>
<div className="absolute top-2 right-2 bg-white/90 backdrop-blur-sm rounded-full shadow-md">
<ListingFavoriteButton listingId={listing.id} />
</div>
</div>
<div className="p-4">
<h3 className="font-semibold text-lg mb-2">{listing.title}</h3>
<p className="text-gray-600 text-sm mb-2 line-clamp-2">
{listing.description}
</p>
<p className="text-lg font-bold">${listing.price}/night</p>
</div>
</Link>
);
}
export default PropertyCard;Added:
bg-white/90- Semi-transparent white backgroundbackdrop-blur-sm- Blur effectshadow-md- Medium shadowrounded-full- Circular container
Result: Button visible on any image color! ✨
Testing the Integration
Let's verify everything works:
Check HomePage
Go to the homepage - each listing card should have a heart button!
Expected:
- Button in top-right of each image
- Gray outline hearts (not favorited yet)
Click a Button
Click a heart button.
Expected:
- Heart turns red and fills
- Navbar counter increases by 1
- Redux DevTools shows ID added to favorites
Click Again
Click the same heart button.
Expected:
- Heart turns gray and unfills
- Navbar counter decreases by 1
- Redux DevTools shows ID removed from favorites
Perfect toggle behavior! 🎉
Check Favorites Page
Favorite a few listings, then go to Favorites page.
Expected:
- Favorited listings shown
- Hearts filled (red)
- Can unfavorite from this page too
Check Details Page
Click a listing to go to details.
Expected:
- Heart button on details page
- Synced with card state (if favorited on card, also favorited on details)
State Synchronization
The beauty of Redux: all buttons stay in sync!
Common Issues & Solutions
What's Next?
Excellent! The favorites feature is complete. In the final lesson, we'll:
- Review all concepts - Redux patterns we learned
- Complete code walkthrough - End-to-end favorites feature
- Best practices - When to use Redux vs local state
- Performance tips - Optimizing Redux apps
✅ Lesson Complete! The favorite button is now integrated into all listing cards!
Key Takeaways
- ✅ Relative + absolute positioning for overlay elements
- ✅ Wrap image in div to create positioning context
- ✅ top-2 right-2 for corner positioning
- ✅ stopPropagation() prevents click bubbling to parent Link
- ✅ Redux synchronization - all buttons stay in sync automatically
- ✅ Reusable component - works in cards, details, anywhere
- ✅ Background/shadow improves visibility on any image