Adopt page filter added
This commit is contained in:
@@ -7,6 +7,7 @@ import org.springframework.http.ResponseEntity;
|
|||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -149,6 +150,25 @@ public class DropdownController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/pet-breeds")
|
||||||
|
public ResponseEntity<List<DropdownOption>> getPetBreeds(@RequestParam(required = false) String species) {
|
||||||
|
if (species == null || species.isBlank()) {
|
||||||
|
|
||||||
|
return ResponseEntity.ok(java.util.Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok(
|
||||||
|
petRepository.findAll().stream()
|
||||||
|
.filter(p -> species.equalsIgnoreCase(p.getPetSpecies()))
|
||||||
|
.map(p -> p.getPetBreed())
|
||||||
|
.filter(breed -> breed != null && !breed.isBlank())
|
||||||
|
.distinct()
|
||||||
|
.sorted(String.CASE_INSENSITIVE_ORDER)
|
||||||
|
.map(breed -> new DropdownOption(null, breed))
|
||||||
|
.collect(Collectors.toList())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/stores")
|
@GetMapping("/stores")
|
||||||
public ResponseEntity<List<DropdownOption>> getStores() {
|
public ResponseEntity<List<DropdownOption>> getStores() {
|
||||||
return ResponseEntity.ok(
|
return ResponseEntity.ok(
|
||||||
|
|||||||
@@ -26,11 +26,12 @@ public class PetController {
|
|||||||
public ResponseEntity<Page<PetResponse>> getAllPets(
|
public ResponseEntity<Page<PetResponse>> getAllPets(
|
||||||
@RequestParam(required = false) String q,
|
@RequestParam(required = false) String q,
|
||||||
@RequestParam(required = false) String species,
|
@RequestParam(required = false) String species,
|
||||||
|
@RequestParam(required = false) String breed,
|
||||||
@RequestParam(required = false) String status,
|
@RequestParam(required = false) String status,
|
||||||
@RequestParam(required = false) Long storeId,
|
@RequestParam(required = false) Long storeId,
|
||||||
@RequestParam(required = false) Long customerId,
|
@RequestParam(required = false) Long customerId,
|
||||||
Pageable pageable) {
|
Pageable pageable) {
|
||||||
return ResponseEntity.ok(petService.getAllPets(q, species, status, storeId, customerId, pageable));
|
return ResponseEntity.ok(petService.getAllPets(q, species, breed, status, storeId, customerId, pageable));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
|
|||||||
@@ -40,21 +40,24 @@ public interface PetRepository extends JpaRepository<Pet, Long> {
|
|||||||
@Query("SELECT p FROM Pet p WHERE " +
|
@Query("SELECT p FROM Pet p WHERE " +
|
||||||
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||||
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
||||||
|
"(:breed IS NULL OR LOWER(COALESCE(p.petBreed, '')) = LOWER(:breed)) AND " +
|
||||||
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " +
|
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " +
|
||||||
"(:storeId IS NULL OR p.store.storeId = :storeId) AND " +
|
"(:storeId IS NULL OR p.store.storeId = :storeId) AND " +
|
||||||
"(:customerId IS NULL OR p.owner.id = :customerId)")
|
"(:customerId IS NULL OR p.owner.id = :customerId)")
|
||||||
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("status") String status, @Param("storeId") Long storeId, @Param("customerId") Long customerId, Pageable pageable);
|
Page<Pet> searchPets(@Param("q") String query, @Param("species") String species, @Param("breed") String breed, @Param("status") String status, @Param("storeId") Long storeId, @Param("customerId") Long customerId, Pageable pageable);
|
||||||
|
|
||||||
@Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " +
|
@Query("SELECT p FROM Pet p WHERE LOWER(p.petStatus) = 'available' AND " +
|
||||||
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||||
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
||||||
|
"(:breed IS NULL OR LOWER(COALESCE(p.petBreed, '')) = LOWER(:breed)) AND " +
|
||||||
"(:storeId IS NULL OR p.store.storeId = :storeId)")
|
"(:storeId IS NULL OR p.store.storeId = :storeId)")
|
||||||
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, @Param("storeId") Long storeId, Pageable pageable);
|
Page<Pet> searchPublicPets(@Param("q") String query, @Param("species") String species, @Param("breed") String breed, @Param("storeId") Long storeId, Pageable pageable);
|
||||||
|
|
||||||
@Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " +
|
@Query("SELECT DISTINCT p FROM Pet p LEFT JOIN Adoption a ON a.pet = p AND LOWER(a.adoptionStatus) = 'completed' WHERE " +
|
||||||
"(LOWER(p.petStatus) = 'available' OR a.customer.id = :userId OR (LOWER(p.petStatus) = 'owned' AND p.owner.id = :userId)) AND " +
|
"(LOWER(p.petStatus) = 'available' OR a.customer.id = :userId OR (LOWER(p.petStatus) = 'owned' AND p.owner.id = :userId)) AND " +
|
||||||
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
"(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||||
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
"(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " +
|
||||||
|
"(:breed IS NULL OR LOWER(COALESCE(p.petBreed, '')) = LOWER(:breed)) AND " +
|
||||||
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
|
"(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status))")
|
||||||
Page<Pet> searchCustomerVisiblePets(@Param("userId") Long userId, @Param("q") String query, @Param("species") String species, @Param("status") String status, Pageable pageable);
|
Page<Pet> searchCustomerVisiblePets(@Param("userId") Long userId, @Param("q") String query, @Param("species") String species, @Param("breed") String breed, @Param("status") String status, Pageable pageable);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ public class SecurityConfig {
|
|||||||
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/v1/services/**").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/v1/categories/**").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-species").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/pet-breeds").permitAll()
|
||||||
|
.requestMatchers(HttpMethod.GET, "/api/v1/dropdowns/stores").permitAll()
|
||||||
.requestMatchers(HttpMethod.GET, "/api/v1/appointments/availability").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/v1/appointments/availability").permitAll()
|
||||||
.anyRequest().authenticated()
|
.anyRequest().authenticated()
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -48,9 +48,10 @@ public class PetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<PetResponse> getAllPets(String query, String species, String status, Long storeId, Long customerId, Pageable pageable) {
|
public Page<PetResponse> getAllPets(String query, String species, String breed, String status, Long storeId, Long customerId, Pageable pageable) {
|
||||||
String normalizedQuery = normalizeFilter(query);
|
String normalizedQuery = normalizeFilter(query);
|
||||||
String normalizedSpecies = normalizeFilter(species);
|
String normalizedSpecies = normalizeFilter(species);
|
||||||
|
String normalizedBreed = normalizeFilter(breed);
|
||||||
String normalizedStatus = normalizeFilter(status);
|
String normalizedStatus = normalizeFilter(status);
|
||||||
CurrentViewer viewer = getCurrentViewer();
|
CurrentViewer viewer = getCurrentViewer();
|
||||||
|
|
||||||
@@ -59,16 +60,16 @@ public class PetService {
|
|||||||
if (!isAllowedPublicStatus(normalizedStatus)) {
|
if (!isAllowedPublicStatus(normalizedStatus)) {
|
||||||
return new PageImpl<>(java.util.List.of(), pageable, 0);
|
return new PageImpl<>(java.util.List.of(), pageable, 0);
|
||||||
}
|
}
|
||||||
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable);
|
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, normalizedBreed, storeId, pageable);
|
||||||
} else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) {
|
} else if (viewer.role() == User.Role.STAFF || viewer.role() == User.Role.ADMIN) {
|
||||||
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedStatus, storeId, customerId, pageable);
|
pets = petRepository.searchPets(normalizedQuery, normalizedSpecies, normalizedBreed, normalizedStatus, storeId, customerId, pageable);
|
||||||
} else if (viewer.role() == User.Role.CUSTOMER) {
|
} else if (viewer.role() == User.Role.CUSTOMER) {
|
||||||
if (!isAllowedCustomerStatus(normalizedStatus)) {
|
if (!isAllowedCustomerStatus(normalizedStatus)) {
|
||||||
return new PageImpl<>(java.util.List.of(), pageable, 0);
|
return new PageImpl<>(java.util.List.of(), pageable, 0);
|
||||||
}
|
}
|
||||||
pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedStatus, pageable);
|
pets = petRepository.searchCustomerVisiblePets(viewer.userId(), normalizedQuery, normalizedSpecies, normalizedBreed, normalizedStatus, pageable);
|
||||||
} else {
|
} else {
|
||||||
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, storeId, pageable);
|
pets = petRepository.searchPublicPets(normalizedQuery, normalizedSpecies, normalizedBreed, storeId, pageable);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pets
|
return pets
|
||||||
|
|||||||
@@ -1,31 +1,62 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import PetCard from "@/components/PetCard";
|
import PetCard from "@/components/PetCard";
|
||||||
import { fetchAllPages } from "@/lib/fetchAllPages";
|
import { fetchAllPages } from "@/lib/fetchAllPages";
|
||||||
|
import { useCart } from "@/context/CartContext";
|
||||||
|
|
||||||
const API_BASE = "";
|
const API_BASE = "";
|
||||||
|
const PAGE_SIZE = 10000;
|
||||||
|
|
||||||
export default function AdoptPage() {
|
export default function AdoptPage() {
|
||||||
|
const { selectedStoreId } = useCart();
|
||||||
|
|
||||||
|
// pets = everything returned by the server (filtered by species + store + text query)
|
||||||
const [pets, setPets] = useState([]);
|
const [pets, setPets] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [health, setHealth] = useState(null);
|
const [health, setHealth] = useState(null);
|
||||||
|
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
|
|
||||||
const PAGE_SIZE = 100;
|
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(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_BASE}/api/v1/health`)
|
fetch(`${API_BASE}/api/v1/health`)
|
||||||
.then((res) => (res.ok ? setHealth("online") : setHealth("error")))
|
.then((res) => (res.ok ? setHealth("online") : setHealth("error")))
|
||||||
.catch(() => setHealth("offline"));
|
.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(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
fetchAllPages((page) => {
|
fetchAllPages((page) => {
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
page: String(page),
|
page: String(page),
|
||||||
@@ -33,25 +64,44 @@ export default function AdoptPage() {
|
|||||||
sort: "id,asc",
|
sort: "id,asc",
|
||||||
status: "Available",
|
status: "Available",
|
||||||
});
|
});
|
||||||
if (query) {
|
if (query) params.set("q", 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}`;
|
return `${API_BASE}/api/v1/pets?${params}`;
|
||||||
})
|
})
|
||||||
.then((allPets) => {
|
.then(setPets)
|
||||||
setPets(allPets);
|
|
||||||
})
|
|
||||||
.catch((err) => setError(err.message))
|
.catch((err) => setError(err.message))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [query]);
|
}, [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) {
|
function handleSearch(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
setQuery(search.trim());
|
setQuery(search.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleClearFilters() {
|
||||||
|
setSearch("");
|
||||||
|
setQuery("");
|
||||||
|
setSelectedSpecies("");
|
||||||
|
setSelectedBreed("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasActiveFilters = query || selectedSpecies || selectedBreed;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="adopt-page">
|
<main className="adopt-page">
|
||||||
<section className="adopt-hero">
|
<section className="adopt-hero">
|
||||||
@@ -61,26 +111,57 @@ export default function AdoptPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="adopt-controls">
|
<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">
|
<div className="adopt-controls-row">
|
||||||
<form className="adopt-search-form" onSubmit={handleSearch}>
|
<form className="adopt-search-form" onSubmit={handleSearch}>
|
||||||
<input
|
<input
|
||||||
className="adopt-search-input"
|
className="adopt-search-input"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search by name, species, or breed..."
|
placeholder="Search by name, species, or breed..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button className="adopt-search-btn" type="submit">Search</button>
|
<button className="adopt-search-btn" type="submit">Search</button>
|
||||||
{query && (
|
|
||||||
<button
|
|
||||||
className="adopt-clear-btn"
|
|
||||||
type="button"
|
|
||||||
onClick={() => { setLoading(true); setError(null); setSearch(""); setQuery(""); }}
|
|
||||||
>
|
|
||||||
Clear
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{hasActiveFilters && (
|
||||||
|
<button className="adopt-clear-btn" type="button" onClick={handleClearFilters}>
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<span
|
<span
|
||||||
className={`backend-status backend-status--${health ?? "checking"}`}
|
className={`backend-status backend-status--${health ?? "checking"}`}
|
||||||
title={
|
title={
|
||||||
@@ -109,13 +190,13 @@ export default function AdoptPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && pets.length === 0 && (
|
{!loading && !error && displayedPets.length === 0 && (
|
||||||
<p className="adopt-status-msg">No pets found.</p>
|
<p className="adopt-status-msg">No pets found matching your filters.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!loading && !error && pets.length > 0 && (
|
{!loading && !error && displayedPets.length > 0 && (
|
||||||
<div className="adopt-grid">
|
<div className="adopt-grid">
|
||||||
{pets.map((pet) => (
|
{displayedPets.map((pet) => (
|
||||||
<PetCard
|
<PetCard
|
||||||
key={pet.petId}
|
key={pet.petId}
|
||||||
petId={pet.petId}
|
petId={pet.petId}
|
||||||
@@ -127,7 +208,6 @@ export default function AdoptPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -824,6 +824,58 @@ body {
|
|||||||
}
|
}
|
||||||
/* Adopt diagnostics */
|
/* Adopt diagnostics */
|
||||||
|
|
||||||
|
.adopt-filters-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adopt-filter-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
min-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adopt-filter-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #555;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adopt-filter-select {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
border: 2px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
background: #fff;
|
||||||
|
color: #333;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
appearance: none;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath fill='%23666' d='M0 0l6 8 6-8z'/%3E%3C/svg%3E");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: right 0.75rem center;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adopt-filter-select:focus {
|
||||||
|
border-color: orange;
|
||||||
|
}
|
||||||
|
|
||||||
|
.adopt-filter-select:disabled {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #aaa;
|
||||||
|
cursor: not-allowed;
|
||||||
|
border-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
.adopt-controls-row {
|
.adopt-controls-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function DisplayNav() {
|
|||||||
value={selectedStoreId ?? ""}
|
value={selectedStoreId ?? ""}
|
||||||
onChange={(e) => setStoreId(e.target.value || null)}
|
onChange={(e) => setStoreId(e.target.value || null)}
|
||||||
>
|
>
|
||||||
<option value="">Select Store</option>
|
<option value="">All Stores</option>
|
||||||
{stores.map((s) => (
|
{stores.map((s) => (
|
||||||
<option key={s.storeId} value={s.storeId}>
|
<option key={s.storeId} value={s.storeId}>
|
||||||
{s.storeName}
|
{s.storeName}
|
||||||
|
|||||||
Reference in New Issue
Block a user