Fix web routing
This commit is contained in:
37
web/app/about/page.js
Normal file
37
web/app/about/page.js
Normal file
@@ -0,0 +1,37 @@
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<main className="info-page">
|
||||
<section className="info-hero">
|
||||
<h1 className="info-title">About Leon's Pet Store</h1>
|
||||
<p className="info-subtitle">Pet care, adoption support, grooming, and everyday essentials in one place.</p>
|
||||
<div className="title-decoration"></div>
|
||||
</section>
|
||||
|
||||
<section className="info-content">
|
||||
<div className="info-card">
|
||||
<h2>What We Do</h2>
|
||||
<p>
|
||||
Leon's Pet Store connects families with adoptable pets, helpful services, and quality products for day-to-day pet care.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="info-card">
|
||||
<h2>Our Focus</h2>
|
||||
<ul className="info-list">
|
||||
<li>Support responsible pet adoption</li>
|
||||
<li>Provide grooming and care services</li>
|
||||
<li>Offer reliable pet supplies and essentials</li>
|
||||
<li>Create a friendly experience for customers and staff</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="info-card">
|
||||
<h2>Visit the Store</h2>
|
||||
<p>
|
||||
Browse adoptable pets, schedule appointments, shop products, or contact the team for help finding the right fit for a pet and household.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -15,8 +15,6 @@ export default function PetDetailPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!id) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetch(`${API_BASE}/api/v1/pets/${id}`)
|
||||
.then((res) => {
|
||||
|
||||
@@ -12,10 +12,8 @@ export default function AdoptPage() {
|
||||
const [health, setHealth] = useState(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [query, setQuery] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
const PAGE_SIZE = 200;
|
||||
|
||||
useEffect(() => {
|
||||
fetch(`${API_BASE}/api/v1/health`)
|
||||
@@ -24,10 +22,7 @@ export default function AdoptPage() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const params = new URLSearchParams({ page, size: PAGE_SIZE, sort: "id,asc" });
|
||||
const params = new URLSearchParams({ page: 0, size: PAGE_SIZE, sort: "petId,asc" });
|
||||
if (query) params.set("q", query);
|
||||
|
||||
fetch(`${API_BASE}/api/v1/pets?${params}`)
|
||||
@@ -37,15 +32,15 @@ export default function AdoptPage() {
|
||||
})
|
||||
.then((data) => {
|
||||
setPets(data.content ?? []);
|
||||
setTotalPages(data.totalPages ?? 0);
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [page, query]);
|
||||
}, [query]);
|
||||
|
||||
function handleSearch(e) {
|
||||
e.preventDefault();
|
||||
setPage(0);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setQuery(search.trim());
|
||||
}
|
||||
|
||||
@@ -72,7 +67,7 @@ export default function AdoptPage() {
|
||||
<button
|
||||
className="adopt-clear-btn"
|
||||
type="button"
|
||||
onClick={() => { setSearch(""); setQuery(""); setPage(0); }}
|
||||
onClick={() => { setLoading(true); setError(null); setSearch(""); setQuery(""); }}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
@@ -125,25 +120,6 @@ export default function AdoptPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && totalPages > 1 && (
|
||||
<div className="adopt-pagination">
|
||||
<button
|
||||
className="pagination-btn"
|
||||
disabled={page === 0}
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
>
|
||||
← Prev
|
||||
</button>
|
||||
<span className="pagination-info">Page {page + 1} of {totalPages}</span>
|
||||
<button
|
||||
className="pagination-btn"
|
||||
disabled={page >= totalPages - 1}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
@@ -68,8 +69,12 @@ function DatePicker({ value, minDate, onChange }) {
|
||||
}
|
||||
|
||||
const cells = [];
|
||||
for (let i = 0; i < firstDay; i++) cells.push(null);
|
||||
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
|
||||
for (let i = 0; i < firstDay; i++) {
|
||||
cells.push({ key: `empty-${viewYear}-${viewMonth}-${String(i)}`, day: null });
|
||||
}
|
||||
for (let d = 1; d <= daysInMonth; d++) {
|
||||
cells.push({ key: `day-${viewYear}-${viewMonth}-${String(d)}`, day: d });
|
||||
}
|
||||
|
||||
const s = {
|
||||
widget: {
|
||||
@@ -160,12 +165,12 @@ function DatePicker({ value, minDate, onChange }) {
|
||||
{DAYS.map((d) => (
|
||||
<span key={d} style={s.dayName}>{d}</span>
|
||||
))}
|
||||
{cells.map((day, i) =>
|
||||
{cells.map(({ key, day }) =>
|
||||
day === null ? (
|
||||
<span key={`empty-${i}`} />
|
||||
<span key={key} />
|
||||
) : (
|
||||
<button
|
||||
key={day}
|
||||
key={key}
|
||||
type="button"
|
||||
style={{
|
||||
...s.dayBase,
|
||||
@@ -190,7 +195,7 @@ function DatePicker({ value, minDate, onChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function AppointmentsPage() {
|
||||
function AppointmentsPage() {
|
||||
const { user, token, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -217,6 +222,8 @@ export default function AppointmentsPage() {
|
||||
const [appointments, setAppointments] = useState([]);
|
||||
const [loadingAppointments, setLoadingAppointments] = useState(false);
|
||||
|
||||
const canBookAppointments = user?.role === "CUSTOMER";
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) {
|
||||
router.push("/login");
|
||||
@@ -241,18 +248,20 @@ export default function AppointmentsPage() {
|
||||
.then((data) => setServices(data.content ?? []))
|
||||
.catch(() => {});
|
||||
|
||||
fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,asc`)
|
||||
fetch(`${API_BASE}/api/v1/pets?size=200&sort=petId,asc`)
|
||||
.then((r) => r.json())
|
||||
.then((data) => setAllPets(data.content ?? []))
|
||||
.catch(() => {});
|
||||
|
||||
fetch(`${API_BASE}/api/v1/my-pets`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => setCustomerPets(Array.isArray(data) ? data : []))
|
||||
.catch(() => {});
|
||||
}, [token]);
|
||||
if (canBookAppointments) {
|
||||
fetch(`${API_BASE}/api/v1/my-pets`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((data) => setCustomerPets(Array.isArray(data) ? data : []))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, [token, canBookAppointments]);
|
||||
|
||||
useEffect(() => {
|
||||
if (didPreselectRef.current) {
|
||||
@@ -366,6 +375,12 @@ export default function AppointmentsPage() {
|
||||
setError(null);
|
||||
setSuccess(null);
|
||||
|
||||
if (!canBookAppointments) {
|
||||
setError("Only customer accounts can book appointments from the web app.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.customerId) {
|
||||
setError("Customer account not found. Please contact support.");
|
||||
|
||||
@@ -458,149 +473,159 @@ export default function AppointmentsPage() {
|
||||
</section>
|
||||
|
||||
<section className="appt-content">
|
||||
<form className="appt-form" onSubmit={handleSubmit}>
|
||||
<h2 className="appt-form-title">New Appointment</h2>
|
||||
{canBookAppointments ? (
|
||||
<form className="appt-form" onSubmit={handleSubmit}>
|
||||
<h2 className="appt-form-title">New Appointment</h2>
|
||||
|
||||
{error && <div className="appt-error">{error}</div>}
|
||||
{success && <div className="appt-success">{success}</div>}
|
||||
{error && <div className="appt-error">{error}</div>}
|
||||
{success && <div className="appt-success">{success}</div>}
|
||||
|
||||
<label className="appt-label">
|
||||
Store Location
|
||||
<select
|
||||
className="appt-select"
|
||||
value={storeId}
|
||||
onChange={(e) => setStoreId(e.target.value)}
|
||||
required
|
||||
<label className="appt-label">
|
||||
Store Location
|
||||
<select
|
||||
className="appt-select"
|
||||
value={storeId}
|
||||
onChange={(e) => setStoreId(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Select a store...</option>
|
||||
{stores.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="appt-label">
|
||||
Service
|
||||
<select
|
||||
className="appt-select"
|
||||
value={serviceId}
|
||||
onChange={(e) => handleServiceChange(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Select a service...</option>
|
||||
{services.map((s) => (
|
||||
<option key={s.serviceId} value={s.serviceId}>
|
||||
{s.serviceName} — ${Number(s.servicePrice).toFixed(2)} ({s.serviceDuration} min)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{selectedService && (
|
||||
<div className="appt-service-info">
|
||||
<p>{selectedService.serviceDesc}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="appt-label">
|
||||
Date
|
||||
<DatePicker
|
||||
value={appointmentDate}
|
||||
minDate={getMinDate()}
|
||||
onChange={setAppointmentDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{storeId && serviceId && appointmentDate && (
|
||||
<div className="appt-label">
|
||||
<span>Available Time Slots</span>
|
||||
{loadingSlots ? (
|
||||
<p className="appt-slots-loading">Checking availability...</p>
|
||||
) : availableSlots.length === 0 ? (
|
||||
<p className="appt-no-slots">No available slots for this date. Please try another date.</p>
|
||||
) : (
|
||||
<div className="appt-slots-grid">
|
||||
{availableSlots.map((slot) => (
|
||||
<button
|
||||
key={slot}
|
||||
type="button"
|
||||
className={`appt-slot-btn ${appointmentTime === slot ? "appt-slot-btn--selected" : ""}`}
|
||||
onClick={() => setAppointmentTime(slot)}
|
||||
>
|
||||
{formatTime(slot)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{serviceId && (
|
||||
<div className="appt-label">
|
||||
<span>{petSectionLabel}</span>
|
||||
{petsToShow.length === 0 ? (
|
||||
<p className="appt-no-slots">{noPetsMessage}</p>
|
||||
) : isAdoptionService ? (
|
||||
<div className="appt-adopt-grid">
|
||||
{petsToShow.map((p) => (
|
||||
<label
|
||||
key={p.petId}
|
||||
className={`appt-adopt-card ${selectedPetIds.includes(p.petId) ? "appt-adopt-card--selected" : ""}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="adoptionPet"
|
||||
value={p.petId}
|
||||
checked={selectedPetIds.includes(p.petId)}
|
||||
onChange={() => togglePet(p.petId)}
|
||||
className="appt-adopt-radio"
|
||||
/>
|
||||
{p.imageUrl ? (
|
||||
<img src={p.imageUrl} alt={p.petName} className="appt-adopt-img" />
|
||||
) : (
|
||||
<div className="appt-adopt-img-placeholder">🐾</div>
|
||||
)}
|
||||
<div className="appt-adopt-info">
|
||||
<span className="appt-adopt-name">{p.petName}</span>
|
||||
<span className="appt-adopt-detail">{p.petSpecies} · {p.petBreed}</span>
|
||||
<span className="appt-adopt-detail">Age: {p.petAge}</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="appt-pets-grid">
|
||||
{petsToShow.map((p) => (
|
||||
<label
|
||||
key={p.customerPetId}
|
||||
className={`appt-pet-chip ${selectedPetIds.includes(p.customerPetId) ? "appt-pet-chip--selected" : ""}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPetIds.includes(p.customerPetId)}
|
||||
onChange={() => togglePet(p.customerPetId)}
|
||||
className="appt-pet-checkbox"
|
||||
/>
|
||||
{p.petName}
|
||||
<span className="appt-pet-chip-species">({p.species})</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="appt-submit-btn"
|
||||
disabled={!formValid || submitting}
|
||||
>
|
||||
<option value="">Select a store...</option>
|
||||
{stores.map((s) => (
|
||||
<option key={s.id} value={s.id}>{s.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="appt-label">
|
||||
Service
|
||||
<select
|
||||
className="appt-select"
|
||||
value={serviceId}
|
||||
onChange={(e) => handleServiceChange(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="">Select a service...</option>
|
||||
{services.map((s) => (
|
||||
<option key={s.serviceId} value={s.serviceId}>
|
||||
{s.serviceName} — ${Number(s.servicePrice).toFixed(2)} ({s.serviceDuration} min)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
{selectedService && (
|
||||
{submitting ? "Booking..." : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"}
|
||||
</button>
|
||||
</form>
|
||||
) : (
|
||||
<div className="appt-form">
|
||||
<h2 className="appt-form-title">Appointment Booking</h2>
|
||||
<div className="appt-service-info">
|
||||
<p>{selectedService.serviceDesc}</p>
|
||||
<p>Web appointment booking is currently available for customer accounts only.</p>
|
||||
<p>Admin and staff accounts can still review appointment activity below.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="appt-label">
|
||||
Date
|
||||
<DatePicker
|
||||
value={appointmentDate}
|
||||
minDate={getMinDate()}
|
||||
onChange={setAppointmentDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{storeId && serviceId && appointmentDate && (
|
||||
<div className="appt-label">
|
||||
<span>Available Time Slots</span>
|
||||
{loadingSlots ? (
|
||||
<p className="appt-slots-loading">Checking availability...</p>
|
||||
) : availableSlots.length === 0 ? (
|
||||
<p className="appt-no-slots">No available slots for this date. Please try another date.</p>
|
||||
) : (
|
||||
<div className="appt-slots-grid">
|
||||
{availableSlots.map((slot) => (
|
||||
<button
|
||||
key={slot}
|
||||
type="button"
|
||||
className={`appt-slot-btn ${appointmentTime === slot ? "appt-slot-btn--selected" : ""}`}
|
||||
onClick={() => setAppointmentTime(slot)}
|
||||
>
|
||||
{formatTime(slot)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{serviceId && (
|
||||
<div className="appt-label">
|
||||
<span>{petSectionLabel}</span>
|
||||
{petsToShow.length === 0 ? (
|
||||
<p className="appt-no-slots">{noPetsMessage}</p>
|
||||
) : isAdoptionService ? (
|
||||
<div className="appt-adopt-grid">
|
||||
{petsToShow.map((p) => (
|
||||
<label
|
||||
key={p.petId}
|
||||
className={`appt-adopt-card ${selectedPetIds.includes(p.petId) ? "appt-adopt-card--selected" : ""}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="adoptionPet"
|
||||
value={p.petId}
|
||||
checked={selectedPetIds.includes(p.petId)}
|
||||
onChange={() => togglePet(p.petId)}
|
||||
className="appt-adopt-radio"
|
||||
/>
|
||||
{p.imageUrl ? (
|
||||
<img src={p.imageUrl} alt={p.petName} className="appt-adopt-img" />
|
||||
) : (
|
||||
<div className="appt-adopt-img-placeholder">🐾</div>
|
||||
)}
|
||||
<div className="appt-adopt-info">
|
||||
<span className="appt-adopt-name">{p.petName}</span>
|
||||
<span className="appt-adopt-detail">{p.petSpecies} · {p.petBreed}</span>
|
||||
<span className="appt-adopt-detail">Age: {p.petAge}</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="appt-pets-grid">
|
||||
{petsToShow.map((p) => (
|
||||
<label
|
||||
key={p.customerPetId}
|
||||
className={`appt-pet-chip ${selectedPetIds.includes(p.customerPetId) ? "appt-pet-chip--selected" : ""}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedPetIds.includes(p.customerPetId)}
|
||||
onChange={() => togglePet(p.customerPetId)}
|
||||
className="appt-pet-checkbox"
|
||||
/>
|
||||
{p.petName}
|
||||
<span className="appt-pet-chip-species">({p.species})</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="appt-submit-btn"
|
||||
disabled={!formValid || submitting}
|
||||
>
|
||||
{submitting ? "Booking..." : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
<div className="appt-history">
|
||||
<h2 className="appt-form-title">Your Appointments</h2>
|
||||
<h2 className="appt-form-title">{canBookAppointments ? "Your Appointments" : "Appointments"}</h2>
|
||||
{loadingAppointments ? (
|
||||
<p className="appt-loading">Loading appointments...</p>
|
||||
) : appointments.length === 0 ? (
|
||||
@@ -637,4 +662,8 @@ export default function AppointmentsPage() {
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default dynamic(() => Promise.resolve(AppointmentsPage), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
73
web/app/contact/page.js
Normal file
73
web/app/contact/page.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const LOCATIONS = [
|
||||
{
|
||||
name: "Downtown Branch",
|
||||
address: "123 Main St",
|
||||
phone: "(123) 456-7890",
|
||||
email: "downtown@petshop.com",
|
||||
},
|
||||
{
|
||||
name: "North Branch",
|
||||
address: "456 North Ave",
|
||||
phone: "(987) 654-3210",
|
||||
email: "north@petshop.com",
|
||||
},
|
||||
{
|
||||
name: "West Side Store",
|
||||
address: "789 West Blvd",
|
||||
phone: "(555) 123-4567",
|
||||
email: "westside@petshop.com",
|
||||
},
|
||||
];
|
||||
|
||||
const PERSONNEL = [
|
||||
{ name: "John Doe", role: "Store Manager" },
|
||||
{ name: "Sara Smith", role: "Staff" },
|
||||
{ name: "Michael Johnson", role: "Grooming Team" },
|
||||
];
|
||||
|
||||
export default function ContactPage() {
|
||||
return (
|
||||
<main className="info-page">
|
||||
<section className="info-hero">
|
||||
<h1 className="info-title">Contact Us</h1>
|
||||
<p className="info-subtitle">Reach the team, find a location, or connect with store personnel.</p>
|
||||
<div className="title-decoration"></div>
|
||||
</section>
|
||||
|
||||
<section className="info-content">
|
||||
<div className="info-card">
|
||||
<h2>General Contact</h2>
|
||||
<p>Email: support@petshop.com</p>
|
||||
<p>Phone: (000) 000-0000</p>
|
||||
<p>Hours: Mon–Sat, 9:00 AM – 6:00 PM</p>
|
||||
</div>
|
||||
|
||||
<div className="info-card">
|
||||
<h2>Store Locations</h2>
|
||||
<div className="info-card-grid">
|
||||
{LOCATIONS.map((location) => (
|
||||
<article key={location.name} className="info-mini-card">
|
||||
<h3>{location.name}</h3>
|
||||
<p>{location.address}</p>
|
||||
<p>{location.phone}</p>
|
||||
<p>{location.email}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-card">
|
||||
<h2>Store Personnel</h2>
|
||||
<div className="info-card-grid">
|
||||
{PERSONNEL.map((person) => (
|
||||
<article key={person.name} className="info-mini-card">
|
||||
<h3>{person.name}</h3>
|
||||
<p>{person.role}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -617,6 +617,74 @@ body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.info-page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(to bottom, #f9f9f9, #ffffff);
|
||||
}
|
||||
|
||||
.info-hero {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem 3rem;
|
||||
}
|
||||
|
||||
.info-title {
|
||||
font-size: 3rem;
|
||||
color: #333;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.info-subtitle {
|
||||
font-size: 1.25rem;
|
||||
color: #666;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 2rem 4rem;
|
||||
display: grid;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.info-card h2 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.info-list {
|
||||
margin: 0;
|
||||
padding-left: 1.2rem;
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.info-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.info-mini-card {
|
||||
background: #fff8ee;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.info-mini-card h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.products-hero {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem 3rem;
|
||||
@@ -1698,4 +1766,4 @@ body {
|
||||
.profile-pet-cancel-btn:hover {
|
||||
border-color: #999;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
@@ -64,10 +65,10 @@ export default function LoginPage() {
|
||||
{loading ? "Logging in…" : "Log In"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
<p className="auth-switch">
|
||||
Don't have an account?{" "}
|
||||
<a href="/register" className="auth-switch-link">Register here</a>
|
||||
<Link href="/register" className="auth-switch-link">Register here</Link>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -4,15 +4,21 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export default function Home() {
|
||||
//Slideshow images array
|
||||
const slideshowImages = [
|
||||
{src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets"},
|
||||
{src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies"},
|
||||
{src: "/images/home/slideshow/pet3.jpg", alt: "Pet grooming"},
|
||||
{src: "/images/home/slideshow/pet4.jpg", alt: "Pet food"},
|
||||
];
|
||||
const slideshowImages = [
|
||||
{id: "slide-1", src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets"},
|
||||
{id: "slide-2", src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies"},
|
||||
{id: "slide-3", src: "/images/home/slideshow/pet3.jpg", alt: "Pet grooming"},
|
||||
{id: "slide-4", src: "/images/home/slideshow/pet4.jpg", alt: "Pet food"},
|
||||
];
|
||||
|
||||
const navImages = [
|
||||
{id: "nav-adopt", src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet"},
|
||||
{id: "nav-products", src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/products", title: "Online Store"},
|
||||
{id: "nav-appointments", src: "/images/home/navimages/appointments.jpg", alt: "Appointments", link: "/appointments", title: "Appointments"},
|
||||
{id: "nav-about", src: "/images/home/navimages/about.jpg", alt: "About Us", link: "/about", title: "About Us"},
|
||||
];
|
||||
|
||||
export default function Home() {
|
||||
const [currentSlide, setCurrentSlide] = useState(0);
|
||||
|
||||
//Auto-advance slideshow
|
||||
@@ -21,15 +27,7 @@ export default function Home() {
|
||||
const timer = setInterval(() => {setCurrentSlide((prev) => (prev + 1) % slideshowImages.length);}, 7500);
|
||||
|
||||
return () => clearInterval(timer);
|
||||
}, [slideshowImages.length]);
|
||||
|
||||
//Hyperlinks to other pages
|
||||
const navImages = [
|
||||
{src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet"},
|
||||
{src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/store", title: "Online Store"},
|
||||
{src: "/images/home/navimages/appointments.jpg", alt: "Appointments", link: "/appointments", title: "Appointments"},
|
||||
{src: "/images/home/navimages/about.jpg", alt: "About Us", link: "/about", title: "About Us"},
|
||||
];
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main className="home-page">
|
||||
@@ -37,7 +35,7 @@ export default function Home() {
|
||||
<section className="slideshow-container">
|
||||
{slideshowImages.map((image, index) => (
|
||||
<div
|
||||
key={index}
|
||||
key={image.id}
|
||||
className={`slide ${index === currentSlide ? "active" : ""}`}
|
||||
>
|
||||
<Image
|
||||
@@ -63,7 +61,7 @@ export default function Home() {
|
||||
<section className="image-links-section">
|
||||
<div className="image-links-container">
|
||||
{navImages.map((item, index) => (
|
||||
<Link href={item.link} key={index} className="image-link-card">
|
||||
<Link href={item.link} key={item.id} className="image-link-card">
|
||||
<div className="image-wrapper">
|
||||
<Image
|
||||
src={item.src}
|
||||
@@ -80,4 +78,4 @@ export default function Home() {
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,9 +17,6 @@ export default function ProductDetailPage() {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
fetch(`${API_BASE}/api/v1/products/${id}`)
|
||||
.then((res) => {
|
||||
|
||||
@@ -11,16 +11,11 @@ export default function ProductsPage() {
|
||||
const [error, setError] = useState(null);
|
||||
const [search, setSearch] = useState("");
|
||||
const [query, setQuery] = useState("");
|
||||
const [page, setPage] = useState(0);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
|
||||
const PAGE_SIZE = 12;
|
||||
const PAGE_SIZE = 200;
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const params = new URLSearchParams({ page, size: PAGE_SIZE, sort: "prodId,asc" });
|
||||
const params = new URLSearchParams({ page: 0, size: PAGE_SIZE, sort: "prodId,asc" });
|
||||
if (query) {
|
||||
params.set("q", query);
|
||||
}
|
||||
@@ -35,15 +30,15 @@ export default function ProductsPage() {
|
||||
})
|
||||
.then((data) => {
|
||||
setProducts(data.content ?? []);
|
||||
setTotalPages(data.totalPages ?? 0);
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
}, [page, query]);
|
||||
}, [query]);
|
||||
|
||||
function handleSearch(e) {
|
||||
e.preventDefault();
|
||||
setPage(0);
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setQuery(search.trim());
|
||||
}
|
||||
|
||||
@@ -70,7 +65,7 @@ export default function ProductsPage() {
|
||||
<button
|
||||
className="adopt-clear-btn"
|
||||
type="button"
|
||||
onClick={() => { setSearch(""); setQuery(""); setPage(0); }}
|
||||
onClick={() => { setLoading(true); setError(null); setSearch(""); setQuery(""); }}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
@@ -108,25 +103,6 @@ export default function ProductsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!loading && totalPages > 1 && (
|
||||
<div className="adopt-pagination">
|
||||
<button
|
||||
className="pagination-btn"
|
||||
disabled={page === 0}
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
>
|
||||
← Prev
|
||||
</button>
|
||||
<span className="pagination-info">Page {page + 1} of {totalPages}</span>
|
||||
<button
|
||||
className="pagination-btn"
|
||||
disabled={page >= totalPages - 1}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
>
|
||||
Next →
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
@@ -140,10 +141,10 @@ export default function RegisterPage() {
|
||||
{loading ? "Creating account…" : "Register"}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
|
||||
<p className="auth-switch">
|
||||
Already have an account?{" "}
|
||||
<a href="/login" className="auth-switch-link">Log in here</a>
|
||||
<Link href="/login" className="auth-switch-link">Log in here</Link>
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user