Styling refactor

This commit is contained in:
augmentedpotato
2026-04-18 16:12:43 -06:00
committed by Harkamal Randhawa
parent 049c39984b
commit 077d147498
21 changed files with 829 additions and 4509 deletions

View File

@@ -15,7 +15,6 @@ export default function PetDetailPage() {
useEffect(() => {
if (!id) return;
fetch(`${API_BASE}/api/v1/pets/${id}`)
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
@@ -27,12 +26,12 @@ export default function PetDetailPage() {
}, [id]);
return (
<main className="pet-detail-page">
<div className="pet-detail-container">
<Link href="/adopt" className="pet-detail-back"> Back to Pets</Link>
<main className="min-h-screen py-12 px-8 pb-20">
<div className="max-w-[860px] mx-auto">
<Link href="/adopt" className="inline-block mb-8 text-[#e68672] no-underline text-base font-semibold transition-colors hover:text-[#d4705e]"> Back to Pets</Link>
{loading && <p className="adopt-status-msg">Loading pet details...</p>}
{error && <p className="adopt-status-msg adopt-error">{error}</p>}
{loading && <p className="text-center text-[#666] text-[1.1rem] py-12">Loading pet details...</p>}
{error && <p className="text-center text-[#c0392b] text-[1.1rem] py-12">{error}</p>}
{!loading && !error && pet && (
<PetProfile

View File

@@ -8,28 +8,37 @@ import { useCart } from "@/context/CartContext";
const API_BASE = "";
const PAGE_SIZE = 10000;
const inputCls = "px-4 py-[0.6rem] border-2 border-[#ddd] rounded-md text-base outline-none transition-colors focus:border-[#e68672]";
const btnPrimaryCls = "px-[1.4rem] py-[0.6rem] bg-[#e68672] text-white border-none rounded-md text-base cursor-pointer transition-colors hover:bg-[#d4705e]";
const btnOutlineCls = "px-4 py-[0.6rem] bg-transparent text-[#666] border-2 border-[#ddd] rounded-md text-base cursor-pointer transition-all hover:border-[#aaa] hover:text-[#333]";
function PaginationBtn({ children, active, ...props }) {
return (
<button
className={`border-none rounded-lg px-[0.9rem] py-2 text-[0.9rem] font-semibold cursor-pointer transition-colors ${active ? "bg-[#e68672] text-white hover:bg-[#d4705e]" : "bg-[#e8e8e8] text-[#333] hover:bg-[#d0d0d0]"} disabled:bg-[#f0f0f0] disabled:text-[#aaa] disabled:cursor-not-allowed`}
{...props}
>
{children}
</button>
);
}
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 [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([]);
useEffect(() => {
setSelectedSpecies("");
const params = new URLSearchParams({ 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) => {
@@ -42,23 +51,16 @@ export default function AdoptPage() {
.catch(() => setSpeciesOptions([]));
}, [selectedStoreId]);
useEffect(() => {
setSelectedBreed("");
}, [selectedSpecies]);
useEffect(() => { setSelectedBreed(""); }, [selectedSpecies]);
useEffect(() => {
setLoading(true);
setError(null);
fetchAllPages((page) => {
const params = new URLSearchParams({
page: String(page),
size: String(PAGE_SIZE),
sort: "id,asc",
});
const params = new URLSearchParams({ page: String(page), size: String(PAGE_SIZE), sort: "id,asc" });
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)
@@ -67,171 +69,123 @@ export default function AdoptPage() {
}, [query, selectedSpecies, selectedStoreId]);
const breedOptions = useMemo(
() =>
[...new Set(pets.map((p) => p.petBreed).filter(Boolean))].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" })
),
() => [...new Set(pets.map((p) => p.petBreed).filter(Boolean))].sort((a, b) =>
a.localeCompare(b, undefined, { sensitivity: "base" })
),
[pets]
);
const ITEMS_PER_PAGE = 24;
const [currentPage, setCurrentPage] = useState(0);
const filteredPets = useMemo(
() => (selectedBreed ? pets.filter((p) => p.petBreed === selectedBreed) : pets),
[pets, selectedBreed]
);
const totalPages = Math.ceil(filteredPets.length / ITEMS_PER_PAGE);
const displayedPets = filteredPets.slice(currentPage * ITEMS_PER_PAGE, (currentPage + 1) * ITEMS_PER_PAGE);
function handleSearch(e) {
e.preventDefault();
setCurrentPage(0);
setQuery(search.trim());
}
function handleClearFilters() {
setSearch("");
setQuery("");
setSelectedSpecies("");
setSelectedBreed("");
setCurrentPage(0);
}
function handleSearch(e) { e.preventDefault(); setCurrentPage(0); setQuery(search.trim()); }
function handleClearFilters() { setSearch(""); setQuery(""); setSelectedSpecies(""); setSelectedBreed(""); setCurrentPage(0); }
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>
<main className="min-h-screen">
<section className="text-center py-16 px-8 bg-gradient-to-b from-[#f9f9f9] to-white">
<h1 className="text-5xl font-bold text-[#333] mb-4 tracking-tight max-[768px]:text-3xl max-[480px]:text-[1.6rem]">Find Your Perfect Companion</h1>
<p className="text-2xl font-light text-[#666] mb-8 max-[768px]:text-[1.2rem]">Give a loving pet their forever home</p>
<div className="w-[100px] h-1 bg-[#e68672] mx-auto mt-8 rounded-sm"></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>
<section className="max-w-[1200px] mx-auto mb-6 px-8">
<div className="flex items-end flex-wrap gap-4 mb-4 max-[600px]:flex-col max-[600px]:items-stretch">
<div className="flex flex-col gap-[0.3rem] min-w-[160px] max-[600px]:min-w-0 max-[600px]:w-full">
<label className="text-[0.8rem] font-semibold text-[#555] uppercase tracking-[0.04em]" htmlFor="species-filter">Species</label>
<select
id="species-filter"
className="adopt-filter-select"
className={`custom-select ${inputCls} bg-white text-[#333] cursor-pointer pr-8`}
value={selectedSpecies}
onChange={(e) => { setSelectedSpecies(e.target.value); setCurrentPage(0); }}
>
<option value="">All Species</option>
{speciesOptions.map((s) => (
<option key={s} value={s}>{s}</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>
<div className="flex flex-col gap-[0.3rem] min-w-[160px] max-[600px]:min-w-0 max-[600px]:w-full">
<label className="text-[0.8rem] font-semibold text-[#555] uppercase tracking-[0.04em]" htmlFor="breed-filter">Breed</label>
<select
id="breed-filter"
className="adopt-filter-select"
className={`custom-select ${inputCls} bg-white text-[#333] cursor-pointer pr-8 disabled:bg-[#f5f5f5] disabled:text-[#aaa] disabled:cursor-not-allowed disabled:border-[#e0e0e0]`}
value={selectedBreed}
onChange={(e) => { setSelectedBreed(e.target.value); setCurrentPage(0); }}
disabled={!selectedSpecies}
>
<option value="">{!selectedSpecies ? "Select a species first" : "All Breeds"}</option>
{breedOptions.map((b) => (
<option key={b} value={b}>{b}</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}>
<div className="flex items-center justify-between flex-wrap gap-4 max-[600px]:flex-col max-[600px]:items-stretch">
<form className="flex gap-3 items-center max-[600px]:flex-col max-[600px]:items-stretch" onSubmit={handleSearch}>
<input
className="adopt-search-input"
className={`${inputCls} flex-1 max-w-[400px] font-[inherit] max-[600px]:max-w-full`}
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>
<button className={`${btnPrimaryCls} font-[inherit] max-[600px]:w-full`} type="submit">Search</button>
</form>
{hasActiveFilters && (
<button className="adopt-clear-btn" type="button" onClick={handleClearFilters}>
<button className={`${btnOutlineCls} font-[inherit] max-[600px]:w-full`} type="button" onClick={handleClearFilters}>
Clear Filters
</button>
)}
</div>
</section>
<section className="adopt-grid-section">
{loading && <p className="adopt-status-msg">Loading pets...</p>}
{error && (
<p className="adopt-status-msg">Unable to load pets, please try again later.</p>
)}
<section className="max-w-[1200px] mx-auto px-8 pb-16">
{loading && <p className="text-center text-[#666] text-[1.1rem] py-12">Loading pets...</p>}
{error && <p className="text-center text-[#666] text-[1.1rem] py-12">Unable to load pets, please try again later.</p>}
{!loading && !error && displayedPets.length === 0 && (
<p className="adopt-status-msg">No pets found matching your filters.</p>
<p className="text-center text-[#666] text-[1.1rem] py-12">No pets found matching your filters.</p>
)}
{!loading && !error && displayedPets.length > 0 && (
<div className="adopt-grid">
<div className="grid grid-cols-4 gap-7 max-[1024px]:grid-cols-3 max-[480px]:grid-cols-2 max-[360px]:grid-cols-1">
{displayedPets.map((pet) => (
<PetCard
key={pet.petId}
petId={pet.petId}
petName={pet.petName}
petSpecies={pet.petSpecies}
petStatus={pet.petStatus}
imageUrl={pet.imageUrl}
/>
<PetCard key={pet.petId} petId={pet.petId} petName={pet.petName} petSpecies={pet.petSpecies} petStatus={pet.petStatus} imageUrl={pet.imageUrl} />
))}
</div>
)}
{!loading && !error && totalPages > 1 && (
<div className="pagination-controls">
<button
className="pagination-btn"
onClick={() => { setCurrentPage((p) => Math.max(0, p - 1)); window.scrollTo(0, 0); }}
disabled={currentPage === 0}
>
<div className="flex items-center justify-center gap-[0.4rem] py-6 px-4 flex-wrap mt-6">
<PaginationBtn onClick={() => { setCurrentPage((p) => Math.max(0, p - 1)); window.scrollTo(0, 0); }} disabled={currentPage === 0}>
Prev
</button>
</PaginationBtn>
{(() => {
const pages = [];
const delta = 2;
const left = Math.max(0, currentPage - delta);
const right = Math.min(totalPages - 1, currentPage + delta);
if (left > 0) {
pages.push(0);
if (left > 1) pages.push("...");
}
if (left > 0) { pages.push(0); if (left > 1) pages.push("..."); }
for (let i = left; i <= right; i++) pages.push(i);
if (right < totalPages - 1) {
if (right < totalPages - 2) pages.push("...");
pages.push(totalPages - 1);
}
if (right < totalPages - 1) { if (right < totalPages - 2) pages.push("..."); pages.push(totalPages - 1); }
return pages.map((p, i) =>
p === "..." ? (
<span key={`ellipsis-${i}`} className="pagination-ellipsis"></span>
<span key={`ellipsis-${i}`} className="px-1 py-2 text-[#888] font-semibold"></span>
) : (
<button
key={p}
className={`pagination-btn${p === currentPage ? " pagination-btn--active" : ""}`}
onClick={() => { setCurrentPage(p); window.scrollTo(0, 0); }}
>
<PaginationBtn key={p} active={p === currentPage} onClick={() => { setCurrentPage(p); window.scrollTo(0, 0); }}>
{p + 1}
</button>
</PaginationBtn>
)
);
})()}
<button
className="pagination-btn"
onClick={() => { setCurrentPage((p) => Math.min(totalPages - 1, p + 1)); window.scrollTo(0, 0); }}
disabled={currentPage === totalPages - 1}
>
<PaginationBtn onClick={() => { setCurrentPage((p) => Math.min(totalPages - 1, p + 1)); window.scrollTo(0, 0); }} disabled={currentPage === totalPages - 1}>
Next
</button>
</PaginationBtn>
</div>
)}
</section>