L12: Add Favorite Button to PropertyCard
Complete the PropertyCard integration with FavoriteButton
Let's complete the PropertyCard with our FavoriteButton! 🎨
Final PropertyCard Component
Here's the complete, polished PropertyCard:
import { Link } from 'react-router-dom';
import FavoriteButton from '@/components/FavoriteButton';
function PropertyCard({ listing }) {
// Fallback for missing data
const image = listing.media?.[0]?.url || '/placeholder.jpg';
const imageAlt = listing.media?.[0]?.alt || listing.name;
const description = listing.description || 'No description available';
const maxGuests = listing.maxGuests || 1;
return (
<div className="listing-card">
{/* Image with Favorite Button */}
<div className="listing-image">
<img src={image} alt={imageAlt} />
{/* Favorite Button - positioned absolutely */}
<FavoriteButton
listingId={listing.id}
size="medium"
className="listing-card__favorite"
/>
</div>
{/* Content */}
<div className="listing-content">
<h3 className="listing-title">{listing.name}</h3>
<p className="listing-description">
{description.length > 100
? `${description.substring(0, 100)}...`
: description
}
</p>
{/* Details */}
<div className="listing-details">
<span className="listing-price">
<strong>${listing.price}</strong>/night
</span>
<span className="listing-guests">
👥 Up to {maxGuests} {maxGuests === 1 ? 'guest' : 'guests'}
</span>
</div>
{/* Optional: Rating */}
{listing.rating > 0 && (
<div className="listing-rating">
⭐ {listing.rating.toFixed(1)}
</div>
)}
</div>
</div>
);
}
export default PropertyCard;Complete Styling
/* Listing Card */
.listing-card {
background: white;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: pointer;
position: relative;
}
.listing-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.15);
}
/* Image Section */
.listing-image {
position: relative;
height: 220px;
overflow: hidden;
background: #f3f4f6;
}
.listing-image img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.listing-card:hover .listing-image img {
transform: scale(1.05);
}
/* Favorite Button Position */
.listing-card__favorite {
position: absolute;
top: 0.75rem;
right: 0.75rem;
}
/* Content Section */
.listing-content {
padding: 1.25rem;
}
.listing-title {
margin: 0 0 0.75rem 0;
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
line-height: 1.4;
/* Limit to 2 lines */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.listing-description {
color: #6b7280;
font-size: 0.95rem;
line-height: 1.5;
margin-bottom: 1rem;
/* Limit to 2 lines */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Details Row */
.listing-details {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
}
.listing-price {
color: #0369a1;
font-size: 1.1rem;
}
.listing-price strong {
font-weight: 700;
font-size: 1.25rem;
}
.listing-guests {
color: #6b7280;
font-size: 0.9rem;
}
/* Rating */
.listing-rating {
margin-top: 0.5rem;
color: #f59e0b;
font-size: 0.9rem;
font-weight: 600;
}
/* Responsive */
@media (max-width: 640px) {
.listing-image {
height: 180px;
}
.listing-content {
padding: 1rem;
}
.listing-title {
font-size: 1.1rem;
}
}How It Works
Testing the Complete Flow
End-to-end test:
- HomePage loads → Listings grid appears
- Hover over card → Card lifts, image zooms
- Click favorite button → Button turns ❤️
- Check navbar → Shows "Favorites [1]"
- Click favorite on 2 more → Navbar shows "Favorites [3]"
- Click "Favorites" in navbar → Navigate to FavoritesPage
- See 3 favorited listings → All show ❤️ buttons
- Click unfavorite → Listing disappears
- Check navbar → Shows "Favorites [2]"
- Click "Home" → Navigate back
- Check listings → One shows 🤍 (unfavorited)
Perfect! Everything works together. ✅
Component Architecture
Component hierarchy:
App
├── Navbar
│ └── FavoriteButton (count badge)
└── Routes
├── HomePage
│ └── PropertyCard (many)
│ └── FavoriteButton
└── FavoritesPage
└── PropertyCard (filtered)
└── FavoriteButtonAll FavoriteButtons share same store! ✨
How data flows:
User clicks FavoriteButton in PropertyCard on HomePage
↓
FavoriteButton calls toggleFavorite(id)
↓
Zustand store updates: favorites = [1, 2, 3, 4]
↓
All components using favorites re-render:
- Navbar badge: "4"
- PropertyCard buttons: Update ❤️/🤍
- FavoritesPage: Shows 4 listings
↓
UI synchronized everywhere!One action, many updates!
Why Zustand makes this easy:
1. No prop drilling:
// ❌ Without Zustand:
<App favorites={favorites}>
<Navbar count={favorites.length} />
<HomePage favorites={favorites} onToggle={handleToggle} />
</App>
// ✅ With Zustand:
<App>
<Navbar /> {/* Reads from store */}
<HomePage /> {/* Reads from store */}
</App>2. Automatic sync:
// All these update automatically:
const count = useListingsStore((state) => state.favorites.length); // Navbar
const isFavorited = favorites.includes(id); // Button
const favoritedItems = getFavoritedItems(); // Page3. Simple code:
// Toggle favorite anywhere:
const toggleFavorite = useListingsStore((state) => state.toggleFavorite);
toggleFavorite(id); // That's it!Clean architecture! ✨
Accessibility Checklist
Performance Optimization
Zustand only re-renders components that use changed data:
// PropertyCard
const isFavorited = favorites.includes(listing.id);
// Re-renders when favorites changes ✅
// Navbar
const count = useListingsStore((state) => state.favorites.length);
// Re-renders when favorites length changes ✅
// HomePage filter
const searchQuery = useListingsStore((state) => state.searchQuery);
// Does NOT re-render when favorites changes ✅Efficient! Only necessary re-renders. 🚀
Optimize expensive calculations:
import { useMemo } from 'react';
function FavoritesPage() {
const items = useListingsStore((state) => state.items);
const favorites = useListingsStore((state) => state.favorites);
// Memoize filtered items
const favoritedItems = useMemo(() => {
return items.filter(item => favorites.includes(item.id));
}, [items, favorites]); // Only recalculate when these change
return <div>{favoritedItems.map(...)}</div>;
}Faster rendering! ⚡
Optimize images:
<img
src={listing.media?.[0]?.url}
alt={listing.media?.[0]?.alt}
loading="lazy" // Lazy load images
decoding="async" // Async decode
/>Better performance! Especially with many listings.
What's Next?
PropertyCard is complete with FavoriteButton integration! In the final lesson:
- Module review - Recap everything we built
- Compare with Redux - See the differences
- Best practices - Key takeaways
- Next steps - What to learn next
✅ Lesson Complete! PropertyCard fully integrated with FavoriteButton!
Key Takeaways
- ✅ Complete integration - FavoriteButton works in PropertyCard
- ✅ Proper positioning - Absolute positioning in top-right
- ✅ Hover effects - Card lifts, image zooms
- ✅ Text clamping - Consistent card heights
- ✅ Responsive design - Works on all screen sizes
- ✅ Accessible - Keyboard navigation, screen readers, color contrast
- ✅ Performant - Selective re-renders, memoization, lazy loading
- ✅ Clean code - Separated concerns, reusable components