Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 60 additions & 43 deletions client/src/pages/AllPerks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,73 @@ import { Link } from 'react-router-dom'
import { api } from '../api'

export default function AllPerks() {


const [perks, setPerks] = useState([])

const [searchQuery, setSearchQuery] = useState('')


const [merchantFilter, setMerchantFilter] = useState('')


const [uniqueMerchants, setUniqueMerchants] = useState([])


const [loading, setLoading] = useState(true)


const [error, setError] = useState('')

// ==================== SIDE EFFECTS WITH useEffect HOOK ====================

/*
TODO: HOOKS TO IMPLEMENT
* useEffect Hook #1: Initial Data Loading
* useEffect Hook #2: Auto-search on Input Change
/*
TODO: HOOKS TO IMPLEMENT
* useEffect Hook #1: Initial Data Loading
* useEffect Hook #2: Auto-search on Input Change

*/

// useEffect Hook #1: Initial Data Loading - Load all perks on component mount
useEffect(() => {
loadAllPerks()
}, [])

// useEffect Hook #2: Auto-search on Input Change - Reload perks when search or filter changes
useEffect(() => {
// Only auto-search if at least one filter has changed (after initial load)
const timer = setTimeout(() => {
loadAllPerks()
}, 500) // 500ms debounce to avoid too many API calls

return () => clearTimeout(timer) // Cleanup timer on dependency change
}, [searchQuery, merchantFilter])

*/


useEffect(() => {
// Extract all merchant names from perks array
const merchants = perks
.map(perk => perk.merchant) // Get merchant from each perk
.filter(merchant => merchant && merchant.trim()) // Remove empty/null values

// Create array of unique merchants using Set
// Set automatically removes duplicates, then we convert back to array
const unique = [...new Set(merchants)]

// Update state with unique merchants
setUniqueMerchants(unique)

// This effect depends on [perks], so it re-runs whenever perks changes
}, [perks]) // Dependency: re-run when perks array changes


async function loadAllPerks() {
// Reset error state before new request
setError('')

// Show loading indicator
setLoading(true)

try {
// Make GET request to /api/perks/all with query parameters
const res = await api.get('/perks/all', {
Expand All @@ -65,15 +80,15 @@ export default function AllPerks() {
merchant: merchantFilter.trim() || undefined
}
})

// Update perks state with response data
setPerks(res.data.perks)

} catch (err) {
// Handle errors (network failure, server error, etc.)
console.error('Failed to load perks:', err)
setError(err?.response?.data?.message || 'Failed to load perks')

} finally {
// This block runs whether try succeeds or catch handles error
// Always stop loading indicator
Expand All @@ -83,17 +98,17 @@ export default function AllPerks() {

// ==================== EVENT HANDLERS ====================


function handleSearch(e) {
// Prevent default form submission behavior (page reload)
e.preventDefault()

// Immediately reload perks with current search and filter values
// This bypasses the debounce delay for instant results
loadAllPerks()
}


function handleReset() {
// Reset search and filter states to empty
// The useEffect with [searchQuery, merchantFilter] dependencies
Expand All @@ -102,16 +117,16 @@ export default function AllPerks() {
setMerchantFilter('')
}



return (
/*
TODO: HTML INPUT HANDLERS
* Update state when user types in search box
* update state when user selects filter
*/
<div className="max-w-6xl mx-auto space-y-6">

{/* Page Title */}
<div className="flex items-center justify-between">
<h1 className="text-3xl font-bold">All Perks</h1>
Expand All @@ -123,10 +138,10 @@ export default function AllPerks() {
{/* Search and Filter Form */}
<div className="card">
<form onSubmit={handleSearch} className="space-y-4">

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">


<div>
<label className="block text-sm font-medium text-zinc-700 mb-2">
<span className="material-symbols-outlined text-sm align-middle">search</span>
Expand All @@ -136,7 +151,8 @@ export default function AllPerks() {
type="text"
className="input"
placeholder="Enter perk name..."

value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<p className="text-xs text-zinc-500 mt-1">
Auto-searches as you type, or press Enter / click Search
Expand All @@ -151,10 +167,11 @@ export default function AllPerks() {
</label>
<select
className="input"

value={merchantFilter}
onChange={(e) => setMerchantFilter(e.target.value)}
>
<option value="">All Merchants</option>

{uniqueMerchants.map(merchant => (
<option key={merchant} value={merchant}>
{merchant}
Expand All @@ -170,15 +187,15 @@ export default function AllPerks() {
<span className="material-symbols-outlined text-sm align-middle">search</span>
{' '}Search Now
</button>
<button
type="button"
<button
type="button"
onClick={handleReset}
className="btn"
>
<span className="material-symbols-outlined text-sm align-middle">refresh</span>
{' '}Reset Filters
</button>

{/* Loading indicator */}
{loading && (
<div className="flex items-center gap-2 text-sm text-zinc-600">
Expand Down Expand Up @@ -207,17 +224,17 @@ export default function AllPerks() {

{/* Perks Grid - Always visible, updates in place */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">

{/*
Conditional Rendering with map():
- If perks.length > 0: Show perk cards
- If perks.length === 0: Show empty state (after the map)
*/}
{perks.map(perk => (

<Link
key={perk._id}

to={`/perks/${perk._id}`}
className="card hover:shadow-lg transition-shadow cursor-pointer"
>
{/* Perk Title */}
Expand All @@ -235,12 +252,12 @@ export default function AllPerks() {
{perk.merchant}
</div>
)}

<div className="flex items-center gap-1">
<span className="material-symbols-outlined text-xs">category</span>
<span className="capitalize">{perk.category}</span>
</div>

{perk.discountPercent > 0 && (
<div className="flex items-center gap-1 text-green-600 font-semibold">
<span className="material-symbols-outlined text-xs">local_offer</span>
Expand All @@ -265,7 +282,7 @@ export default function AllPerks() {
</Link>
))}


{perks.length === 0 && !loading && (
<div className="col-span-full text-center py-12 text-zinc-600">
<span className="material-symbols-outlined text-5xl mb-4 block text-zinc-400">
Expand All @@ -276,7 +293,7 @@ export default function AllPerks() {
</div>
)}


{loading && perks.length === 0 && (
<div className="col-span-full text-center py-12 text-zinc-600">
<span className="material-symbols-outlined text-5xl mb-4 block text-zinc-400 animate-spin">
Expand Down