Files
group-2-threaded-project-pe…/web/app/adopt/page.js
2026-04-13 10:34:26 -06:00

215 lines
7.2 KiB
JavaScript

"use client";
import { useState, useEffect, useMemo } from "react";
import PetCard from "@/components/PetCard";
import { fetchAllPages } from "@/lib/fetchAllPages";
import { useCart } from "@/context/CartContext";
const API_BASE = "";
const PAGE_SIZE = 10000;
export default function AdoptPage() {
const { selectedStoreId } = useCart();
// pets = everything returned by the server (filtered by species + store + text query)
const [pets, setPets] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [health, setHealth] = useState(null);
const [search, setSearch] = useState("");
const [query, setQuery] = useState("");
const [selectedSpecies, setSelectedSpecies] = useState("");
const [selectedBreed, setSelectedBreed] = useState("");
// Species options come from a dedicated fetch (only store-filtered, no species filter)
const [speciesOptions, setSpeciesOptions] = useState([]);
// ---------- health check ----------
useEffect(() => {
fetch(`${API_BASE}/api/v1/health`)
.then((res) => (res.ok ? setHealth("online") : setHealth("error")))
.catch(() => setHealth("offline"));
}, []);
useEffect(() => {
setSelectedSpecies("");
const params = new URLSearchParams({ status: "Available", page: "0", size: String(PAGE_SIZE) });
if (selectedStoreId) params.set("storeId", String(selectedStoreId));
fetch(`${API_BASE}/api/v1/pets?${params}`)
.then((r) => (r.ok ? r.json() : null))
.then((data) => {
const items = data?.content ?? [];
const species = [...new Set(items.map((p) => p.petSpecies).filter(Boolean))].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" })
);
setSpeciesOptions(species);
})
.catch(() => setSpeciesOptions([]));
}, [selectedStoreId]);
useEffect(() => {
setSelectedBreed("");
}, [selectedSpecies]);
useEffect(() => {
setLoading(true);
setError(null);
fetchAllPages((page) => {
const params = new URLSearchParams({
page: String(page),
size: String(PAGE_SIZE),
sort: "id,asc",
status: "Available",
});
if (query) params.set("q", query);
if (selectedSpecies) params.set("species", selectedSpecies);
if (selectedStoreId) params.set("storeId", String(selectedStoreId));
return `${API_BASE}/api/v1/pets?${params}`;
})
.then(setPets)
.catch((err) => setError(err.message))
.finally(() => setLoading(false));
}, [query, selectedSpecies, selectedStoreId]);
const breedOptions = useMemo(
() =>
[...new Set(pets.map((p) => p.petBreed).filter(Boolean))].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" })
),
[pets]
);
const displayedPets = useMemo(
() => (selectedBreed ? pets.filter((p) => p.petBreed === selectedBreed) : pets),
[pets, selectedBreed]
);
function handleSearch(e) {
e.preventDefault();
setQuery(search.trim());
}
function handleClearFilters() {
setSearch("");
setQuery("");
setSelectedSpecies("");
setSelectedBreed("");
}
const hasActiveFilters = query || selectedSpecies || selectedBreed;
return (
<main className="adopt-page">
<section className="adopt-hero">
<h1 className="adopt-hero-title">Find Your Perfect Companion</h1>
<p className="adopt-hero-subtitle">Give a loving pet their forever home</p>
<div className="title-decoration"></div>
</section>
<section className="adopt-controls">
<div className="adopt-filters-row">
<div className="adopt-filter-group">
<label className="adopt-filter-label" htmlFor="species-filter">Species</label>
<select
id="species-filter"
className="adopt-filter-select"
value={selectedSpecies}
onChange={(e) => setSelectedSpecies(e.target.value)}
>
<option value="">All Species</option>
{speciesOptions.map((s) => (
<option key={s} value={s}>{s}</option>
))}
</select>
</div>
<div className="adopt-filter-group">
<label className="adopt-filter-label" htmlFor="breed-filter">Breed</label>
<select
id="breed-filter"
className="adopt-filter-select"
value={selectedBreed}
onChange={(e) => setSelectedBreed(e.target.value)}
disabled={!selectedSpecies}
>
<option value="">{!selectedSpecies ? "Select a species first" : "All Breeds"}</option>
{breedOptions.map((b) => (
<option key={b} value={b}>{b}</option>
))}
</select>
</div>
</div>
<div className="adopt-controls-row">
<form className="adopt-search-form" onSubmit={handleSearch}>
<input
className="adopt-search-input"
type="text"
placeholder="Search by name, species, or breed..."
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<button className="adopt-search-btn" type="submit">Search</button>
</form>
{hasActiveFilters && (
<button className="adopt-clear-btn" type="button" onClick={handleClearFilters}>
Clear Filters
</button>
)}
<span
className={`backend-status backend-status--${health ?? "checking"}`}
title={
health === "online" ? "Backend online" :
health === "offline" ? "Backend offline" :
health === "error" ? "Backend error" : "Checking…"
}
/>
</div>
</section>
<section className="adopt-grid-section">
{loading && <p className="adopt-status-msg">Loading pets...</p>}
{error && (
<div className="adopt-error-box">
<p className="adopt-error-title">Failed to load pets</p>
<code className="adopt-error-detail">{error}</code>
<p className="adopt-error-hint">
{health === "offline"
? "The Spring Boot backend is not reachable. Make sure it is running in IntelliJ on port 8080."
: health === "error"
? "The backend responded with an error. Check the IntelliJ Run console for stack traces."
: "The backend is reachable but the /pets endpoint failed. Check the IntelliJ Run console."}
</p>
</div>
)}
{!loading && !error && displayedPets.length === 0 && (
<p className="adopt-status-msg">No pets found matching your filters.</p>
)}
{!loading && !error && displayedPets.length > 0 && (
<div className="adopt-grid">
{displayedPets.map((pet) => (
<PetCard
key={pet.petId}
petId={pet.petId}
petName={pet.petName}
petSpecies={pet.petSpecies}
petStatus={pet.petStatus}
imageUrl={pet.imageUrl}
/>
))}
</div>
)}
</section>
</main>
);
}