L7: M2 Challenge & Wrap-up
Apply your skills with a comprehensive challenge
Put everything you've learned together in this comprehensive challenge! You'll enhance StaySense with additional features using state management, event handling, and filtering techniques.
šÆ Challenge Overview
Enhance your StaySense application with these features:
Required Features
-
"Save for Later" Functionality
- Add a heart icon to each listing card
- Click to toggle saved status
- Track saved listings in state
- Show count of saved listings
-
Advanced Filtering
- Add price range filter (min/max)
- Add property type filter (house/cabin/loft/all)
- Combine all filters seamlessly
-
Sort Options
- Sort by price (low to high, high to low)
- Sort by guest capacity
- Sort by name (A-Z)
Bonus Features (Optional)
-
Filter Summary
- Show active filters as removable chips
- Click chip to remove individual filter
-
View Saved Listings
- Toggle between "All Listings" and "Saved Listings"
- Filter works on saved view too
š Requirements Checklist
Before you begin, make sure you have:
- ā Completed Lessons 1-6
- ā Working filter system
- ā Understanding of state management
- ā Familiarity with array methods
š Challenge Instructions
Part 1: Save for Later Feature
Goal: Let users save their favorite listings.
Steps:
- Add
savedListingsstate to HomePage (array of IDs):
const [savedListings, setSavedListings] = useState([]);- Create toggle function:
const toggleSaved = (listingId) => {
setSavedListings(prev => {
if (prev.includes(listingId)) {
return prev.filter(id => id !== listingId);
} else {
return [...prev, listingId];
}
});
};- Pass to PropertyCard:
<PropertyCard
listing={listing}
isSaved={savedListings.includes(listing.id)}
onToggleSave={toggleSaved}
/>- Update PropertyCard with heart button:
export function PropertyCard({ listing, isSaved, onToggleSave }) {
return (
<div className="card">
<button
onClick={() => onToggleSave(listing.id)}
className="absolute top-2 right-2 z-10"
>
{isSaved ? 'ā¤ļø' : 'š¤'}
</button>
{/* Rest of card... */}
</div>
);
}Test: Click hearts to save/unsave listings.
Part 2: Price Range Filter
Goal: Filter by minimum and maximum price.
Steps:
- Add price state to HomePage:
const [minPrice, setMinPrice] = useState(0);
const [maxPrice, setMaxPrice] = useState(1000);- Add price inputs to ListingFilters:
<div className="grid grid-cols-2 gap-4 mb-4">
<div>
<label>Min Price</label>
<input
type="number"
value={minPrice}
onChange={(e) => onMinPriceChange(Number(e.target.value))}
min="0"
/>
</div>
<div>
<label>Max Price</label>
<input
type="number"
value={maxPrice}
onChange={(e) => onMaxPriceChange(Number(e.target.value))}
min="0"
/>
</div>
</div>- Update filtering logic:
const matchesPrice =
listing.price >= minPrice &&
listing.price <= maxPrice;Test: Adjust price range to filter listings.
Part 3: Property Type Filter
Goal: Filter by property type.
Steps:
- Add property type to listings data:
{
id: 1,
title: "Cozy Beach House",
type: "house", // Add this
// ... rest
}- Add type state:
const [propertyType, setPropertyType] = useState('all');- Add select dropdown to ListingFilters:
<div className="mb-4">
<label>Property Type</label>
<select
value={propertyType}
onChange={(e) => onPropertyTypeChange(e.target.value)}
className="w-full px-4 py-2 border rounded-lg"
>
<option value="all">All Types</option>
<option value="house">House</option>
<option value="cabin">Cabin</option>
<option value="loft">Loft</option>
</select>
</div>- Update filter logic:
const matchesType =
propertyType === 'all' ||
listing.type === propertyType;Test: Select different property types.
Part 4: Sort Functionality
Goal: Allow users to sort filtered results.
Steps:
- Add sort state:
const [sortBy, setSortBy] = useState('default');- Create sort function:
const getSortedListings = (listings) => {
const sorted = [...listings];
switch(sortBy) {
case 'price-low':
return sorted.sort((a, b) => a.price - b.price);
case 'price-high':
return sorted.sort((a, b) => b.price - a.price);
case 'guests':
return sorted.sort((a, b) => b.guests - a.guests);
case 'name':
return sorted.sort((a, b) => a.title.localeCompare(b.title));
default:
return sorted;
}
};- Apply sorting after filtering:
const filteredListings = getFilteredListings();
const sortedListings = getSortedListings(filteredListings);- Add sort dropdown to filters:
<div className="mb-4">
<label>Sort By</label>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value)}
className="w-full px-4 py-2 border rounded-lg"
>
<option value="default">Default</option>
<option value="price-low">Price: Low to High</option>
<option value="price-high">Price: High to Low</option>
<option value="guests">Guest Capacity</option>
<option value="name">Name (A-Z)</option>
</select>
</div>Test: Try different sort options.
Part 5: View Saved Listings (Bonus)
Goal: Toggle between all listings and saved only.
Steps:
- Add view state:
const [view, setView] = useState('all'); // 'all' or 'saved'- Add toggle buttons:
<div className="flex gap-2 mb-4">
<button
onClick={() => setView('all')}
className={view === 'all' ? 'active' : ''}
>
All Listings ({listings.length})
</button>
<button
onClick={() => setView('saved')}
className={view === 'saved' ? 'active' : ''}
>
Saved ({savedListings.length})
</button>
</div>- Filter by view:
const getViewListings = () => {
if (view === 'saved') {
return listings.filter(l => savedListings.includes(l.id));
}
return listings;
};
const viewListings = getViewListings();
const filteredListings = viewListings.filter(/* filter logic */);Test: Save some listings, switch to saved view.
Part 6: Filter Chips (Bonus)
Goal: Show active filters as removable chips.
Steps:
- Create ActiveFilters component:
function ActiveFilters({ filters, onRemove }) {
return (
<div className="flex flex-wrap gap-2 mb-4">
{filters.map(filter => (
<div
key={filter.id}
className="bg-blue-100 px-3 py-1 rounded-full flex items-center gap-2"
>
<span>{filter.label}</span>
<button onClick={() => onRemove(filter.id)}>Ć</button>
</div>
))}
</div>
);
}- Build active filters array:
const getActiveFilters = () => {
const filters = [];
if (search) {
filters.push({ id: 'search', label: `Search: ${search}` });
}
if (guests > 1) {
filters.push({ id: 'guests', label: `${guests} guests` });
}
if (propertyType !== 'all') {
filters.push({ id: 'type', label: `Type: ${propertyType}` });
}
return filters;
};- Implement remove handler:
const handleRemoveFilter = (filterId) => {
switch(filterId) {
case 'search':
setSearch('');
break;
case 'guests':
setGuests(1);
break;
case 'type':
setPropertyType('all');
break;
}
};Test: Apply filters, remove via chips.
ā Challenge Completion Checklist
Required Features:
Bonus Features:
Code Quality:
š” Hints & Tips
šÆ Solution Preview
š Congratulations!
By completing this challenge, you've demonstrated mastery of:
- ā Complex state management
- ā Multiple filter coordination
- ā Sorting algorithms
- ā Array manipulation
- ā Component communication
- ā User experience patterns
You're Ready for Module 3!
In Module 3, you'll learn:
- useEffect hook for side effects
- Fetching data from APIs
- Handling async operations
- Managing loading and error states
- Building advanced components
Take a break and celebrate your progress! š
When you're ready, continue to Module 3: Effects & Data Fetching.