From 3ee59521fd4b1b6db56ef92dd90682549071a2b4 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 08:54:24 -0600 Subject: [PATCH 1/5] Fix web routing --- web/app/about/page.js | 37 ++++ web/app/adopt/[id]/page.js | 2 - web/app/adopt/page.js | 36 +--- web/app/appointments/page.js | 329 ++++++++++++++++++---------------- web/app/contact/page.js | 73 ++++++++ web/app/globals.css | 70 +++++++- web/app/login/page.js | 5 +- web/app/page.js | 38 ++-- web/app/products/[id]/page.js | 3 - web/app/products/page.js | 36 +--- web/app/register/page.js | 5 +- web/components/Navigation.js | 25 +-- 12 files changed, 407 insertions(+), 252 deletions(-) create mode 100644 web/app/about/page.js create mode 100644 web/app/contact/page.js diff --git a/web/app/about/page.js b/web/app/about/page.js new file mode 100644 index 00000000..5ba61fe2 --- /dev/null +++ b/web/app/about/page.js @@ -0,0 +1,37 @@ +export default function AboutPage() { + return ( +
+
+

About Leon's Pet Store

+

Pet care, adoption support, grooming, and everyday essentials in one place.

+
+
+ +
+
+

What We Do

+

+ Leon's Pet Store connects families with adoptable pets, helpful services, and quality products for day-to-day pet care. +

+
+ +
+

Our Focus

+
    +
  • Support responsible pet adoption
  • +
  • Provide grooming and care services
  • +
  • Offer reliable pet supplies and essentials
  • +
  • Create a friendly experience for customers and staff
  • +
+
+ +
+

Visit the Store

+

+ Browse adoptable pets, schedule appointments, shop products, or contact the team for help finding the right fit for a pet and household. +

+
+
+
+ ); +} diff --git a/web/app/adopt/[id]/page.js b/web/app/adopt/[id]/page.js index 0834ca01..6c7b70e0 100644 --- a/web/app/adopt/[id]/page.js +++ b/web/app/adopt/[id]/page.js @@ -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) => { diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index 430a1653..90293d8c 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -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() { @@ -125,25 +120,6 @@ export default function AdoptPage() { )} - {!loading && totalPages > 1 && ( -
- - Page {page + 1} of {totalPages} - -
- )} ); diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index d3f7943e..bec8bd78 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -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) => ( {d} ))} - {cells.map((day, i) => + {cells.map(({ key, day }) => day === null ? ( - + ) : ( + ))} + + )} + + )} + + {serviceId && ( +
+ {petSectionLabel} + {petsToShow.length === 0 ? ( +

{noPetsMessage}

+ ) : isAdoptionService ? ( +
+ {petsToShow.map((p) => ( + + ))} +
+ ) : ( +
+ {petsToShow.map((p) => ( + + ))} +
+ )} +
+ )} + + + + ) : ( +
+

Appointment Booking

-

{selectedService.serviceDesc}

+

Web appointment booking is currently available for customer accounts only.

+

Admin and staff accounts can still review appointment activity below.

- )} - -
- Date -
- - {storeId && serviceId && appointmentDate && ( -
- Available Time Slots - {loadingSlots ? ( -

Checking availability...

- ) : availableSlots.length === 0 ? ( -

No available slots for this date. Please try another date.

- ) : ( -
- {availableSlots.map((slot) => ( - - ))} -
- )} -
- )} - - {serviceId && ( -
- {petSectionLabel} - {petsToShow.length === 0 ? ( -

{noPetsMessage}

- ) : isAdoptionService ? ( -
- {petsToShow.map((p) => ( - - ))} -
- ) : ( -
- {petsToShow.map((p) => ( - - ))} -
- )} -
- )} - - - + )}
-

Your Appointments

+

{canBookAppointments ? "Your Appointments" : "Appointments"}

{loadingAppointments ? (

Loading appointments...

) : appointments.length === 0 ? ( @@ -637,4 +662,8 @@ export default function AppointmentsPage() { ); -} \ No newline at end of file +} + +export default dynamic(() => Promise.resolve(AppointmentsPage), { + ssr: false, +}); diff --git a/web/app/contact/page.js b/web/app/contact/page.js new file mode 100644 index 00000000..85fba175 --- /dev/null +++ b/web/app/contact/page.js @@ -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 ( +
+
+

Contact Us

+

Reach the team, find a location, or connect with store personnel.

+
+
+ +
+
+

General Contact

+

Email: support@petshop.com

+

Phone: (000) 000-0000

+

Hours: Mon–Sat, 9:00 AM – 6:00 PM

+
+ +
+

Store Locations

+
+ {LOCATIONS.map((location) => ( +
+

{location.name}

+

{location.address}

+

{location.phone}

+

{location.email}

+
+ ))} +
+
+ +
+

Store Personnel

+
+ {PERSONNEL.map((person) => ( +
+

{person.name}

+

{person.role}

+
+ ))} +
+
+
+
+ ); +} diff --git a/web/app/globals.css b/web/app/globals.css index 500ccbb0..da8ce03b 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -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; -} \ No newline at end of file +} diff --git a/web/app/login/page.js b/web/app/login/page.js index 32c05c52..dcc78d02 100644 --- a/web/app/login/page.js +++ b/web/app/login/page.js @@ -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"} - +

Don't have an account?{" "} - Register here + Register here

diff --git a/web/app/page.js b/web/app/page.js index 4819bd7d..91f71946 100644 --- a/web/app/page.js +++ b/web/app/page.js @@ -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 (
@@ -37,7 +35,7 @@ export default function Home() {
{slideshowImages.map((image, index) => (
{navImages.map((item, index) => ( - +
); -} \ No newline at end of file +} diff --git a/web/app/products/[id]/page.js b/web/app/products/[id]/page.js index 833cbd24..47a3059b 100644 --- a/web/app/products/[id]/page.js +++ b/web/app/products/[id]/page.js @@ -17,9 +17,6 @@ export default function ProductDetailPage() { if (!id) { return; } - - setLoading(true); - setError(null); fetch(`${API_BASE}/api/v1/products/${id}`) .then((res) => { diff --git a/web/app/products/page.js b/web/app/products/page.js index f44475f1..b3416326 100644 --- a/web/app/products/page.js +++ b/web/app/products/page.js @@ -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() { @@ -108,25 +103,6 @@ export default function ProductsPage() {
)} - {!loading && totalPages > 1 && ( -
- - Page {page + 1} of {totalPages} - -
- )} ); diff --git a/web/app/register/page.js b/web/app/register/page.js index 20dcd2c1..eb9de480 100644 --- a/web/app/register/page.js +++ b/web/app/register/page.js @@ -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"} - +

Already have an account?{" "} - Log in here + Log in here

diff --git a/web/components/Navigation.js b/web/components/Navigation.js index cfd17924..3259c1d1 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -1,6 +1,7 @@ "use client"; import Image from "next/image"; +import Link from "next/link"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -23,31 +24,31 @@ export default function DisplayNav() { id="logo"/>
- Home - Adopt a Pet - Online Store - Schedule an Appointment - Contact Us - About Us + Home + Adopt a Pet + Online Store + Schedule an Appointment + Contact Us + About Us
{loading ? null : user ? ( <> - + Hello, {user.fullName || user.username} - - ) : ( <> - Log In - Register + Log In + Register )}
); -} \ No newline at end of file +} -- 2.49.1 From a76895434d28d7b4ea233f3b68dbd40da536319d Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:06:24 -0600 Subject: [PATCH 2/5] Improve auth flows --- web/app/appointments/page.js | 5 +- web/app/globals.css | 38 +++++++ web/app/login/page.js | 24 ++++- web/app/profile/page.js | 194 +++++++++++++++++++++++++++++++++-- web/app/register/page.js | 24 ++++- web/context/AuthContext.js | 54 ++++++---- 6 files changed, 300 insertions(+), 39 deletions(-) diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index bec8bd78..fac5a86f 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -226,10 +226,11 @@ function AppointmentsPage() { useEffect(() => { if (!authLoading && !user) { - router.push("/login"); + const target = preselectedPetId ? `/appointments?petId=${encodeURIComponent(preselectedPetId)}` : "/appointments"; + router.push(`/login?next=${encodeURIComponent(target)}`); } - }, [authLoading, user, router]); + }, [authLoading, user, router, preselectedPetId]); useEffect(() => { if (!token) { diff --git a/web/app/globals.css b/web/app/globals.css index da8ce03b..8571bfeb 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -1076,6 +1076,13 @@ body { align-items: center; justify-content: center; margin-bottom: 0.25rem; + overflow: hidden; +} + +.profile-avatar-image { + width: 100%; + height: 100%; + object-fit: cover; } .profile-name { @@ -1105,6 +1112,37 @@ body { padding-top: 1rem; } +.profile-update-form { + width: 100%; + display: grid; + gap: 0.9rem; +} + +.profile-update-title { + margin: 0.25rem 0 0; + font-size: 1.1rem; + color: #222; +} + +.profile-avatar-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.75rem; +} + +.profile-avatar-upload-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 0.65rem 1rem; + border-radius: 8px; + background: #fff3e0; + color: #a65c00; + font-weight: 600; + cursor: pointer; +} + .profile-field-row { display: flex; justify-content: space-between; diff --git a/web/app/login/page.js b/web/app/login/page.js index dcc78d02..2e7d1fe1 100644 --- a/web/app/login/page.js +++ b/web/app/login/page.js @@ -1,13 +1,25 @@ "use client"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; -export default function LoginPage() { +function resolveNextPath(candidate) { + if (!candidate || !candidate.startsWith("/")) { + return "/"; + } + if (candidate.startsWith("//") || candidate.startsWith("/login") || candidate.startsWith("/register")) { + return "/"; + } + return candidate; +} + +function LoginPage() { const {login} = useAuth(); const router = useRouter(); + const searchParams = useSearchParams(); const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); @@ -21,7 +33,7 @@ export default function LoginPage() { try { await login(username, password); - router.push("/"); + router.push(resolveNextPath(searchParams.get("next"))); } catch (err) { @@ -68,9 +80,13 @@ export default function LoginPage() {

Don't have an account?{" "} - Register here + Register here

); } + +export default dynamic(() => Promise.resolve(LoginPage), { + ssr: false, +}); diff --git a/web/app/profile/page.js b/web/app/profile/page.js index 6b28aa35..d0b78c5e 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -7,7 +7,7 @@ import { useAuth } from "@/context/AuthContext"; const API_BASE = ""; export default function ProfilePage() { - const {user, token, loading, logout} = useAuth(); + const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); const [pets, setPets] = useState([]); @@ -19,14 +19,27 @@ export default function ProfilePage() { const [breed, setBreed] = useState(""); const [submitting, setSubmitting] = useState(false); const [petError, setPetError] = useState(null); + const [profileForm, setProfileForm] = useState({ fullName: "", email: "", phone: "" }); + const [profileSubmitting, setProfileSubmitting] = useState(false); + const [profileError, setProfileError] = useState(null); + const [profileSuccess, setProfileSuccess] = useState(null); + const [avatarSubmitting, setAvatarSubmitting] = useState(false); useEffect(() => { if (!loading && !user) { - router.replace("/login"); + router.replace(`/login?next=${encodeURIComponent("/profile")}`); } }, [user, loading, router]); + useEffect(() => { + setProfileForm({ + fullName: user?.fullName || "", + email: user?.email || "", + phone: user?.phone || "", + }); + }, [user]); + const loadPets = useCallback(() => { if (!token) return; setLoadingPets(true); @@ -50,6 +63,105 @@ export default function ProfilePage() { router.push("/"); } + async function handleProfileSubmit(e) { + e.preventDefault(); + setProfileSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify(profileForm), + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || `Request failed (${res.status})`); + } + + await refreshUser(); + setProfileSuccess("Profile updated successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setProfileSubmitting(false); + } + } + + async function handleAvatarUpload(file) { + if (!file) { + return; + } + + const formData = new FormData(); + formData.append("avatar", file); + setAvatarSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me/avatar`, { + method: "POST", + headers: { Authorization: `Bearer ${token}` }, + body: formData, + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || "Failed to upload avatar"); + } + + await refreshUser(); + setProfileSuccess(data?.message || "Avatar updated successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setAvatarSubmitting(false); + } + } + + async function handleAvatarDelete() { + setAvatarSubmitting(true); + setProfileError(null); + setProfileSuccess(null); + + try { + const res = await fetch(`${API_BASE}/api/v1/auth/me/avatar`, { + method: "DELETE", + headers: { Authorization: `Bearer ${token}` }, + }); + + const data = await res.json().catch(() => null); + if (!res.ok) { + throw new Error(data?.message || "Failed to delete avatar"); + } + + await refreshUser(); + setProfileSuccess(data?.message || "Avatar removed successfully."); + } + + catch (err) { + setProfileError(err.message); + } + + finally { + setAvatarSubmitting(false); + } + } + function openAddForm() { setEditingPet(null); setPetName(""); @@ -170,7 +282,11 @@ export default function ProfilePage() {
- {(user.fullName || user.username).charAt(0).toUpperCase()} + {user.avatarUrl ? ( + {user.fullName + ) : ( + (user.fullName || user.username).charAt(0).toUpperCase() + )}

{user.fullName || user.username}

@@ -185,7 +301,65 @@ export default function ProfilePage() { ))} - + )} +
+ + + + @@ -194,7 +368,7 @@ export default function ProfilePage() {

My Pets

- +
{showForm && ( @@ -285,11 +459,11 @@ export default function ProfilePage() { {pet.breed && {pet.breed}}
- - -
- - ))} + + + + + ))} )} diff --git a/web/app/register/page.js b/web/app/register/page.js index eb9de480..17f49672 100644 --- a/web/app/register/page.js +++ b/web/app/register/page.js @@ -1,13 +1,25 @@ "use client"; +import dynamic from "next/dynamic"; import Link from "next/link"; import { useState } from "react"; -import { useRouter } from "next/navigation"; +import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; -export default function RegisterPage() { +function resolveNextPath(candidate) { + if (!candidate || !candidate.startsWith("/")) { + return "/"; + } + if (candidate.startsWith("//") || candidate.startsWith("/login") || candidate.startsWith("/register")) { + return "/"; + } + return candidate; +} + +function RegisterPage() { const {register} = useAuth(); const router = useRouter(); + const searchParams = useSearchParams(); const [form, setForm] = useState({ fullName: "", @@ -41,7 +53,7 @@ export default function RegisterPage() { phone: form.phone, password: form.password, }); - router.push("/"); + router.push(resolveNextPath(searchParams.get("next"))); } catch (err) { @@ -144,9 +156,13 @@ export default function RegisterPage() {

Already have an account?{" "} - Log in here + Log in here

); } + +export default dynamic(() => Promise.resolve(RegisterPage), { + ssr: false, +}); diff --git a/web/context/AuthContext.js b/web/context/AuthContext.js index 5a9b0d08..e861f624 100644 --- a/web/context/AuthContext.js +++ b/web/context/AuthContext.js @@ -19,6 +19,28 @@ export function AuthProvider({ children }) { const [token, setToken] = useState(null); const [loading, setLoading] = useState(true); + const refreshUser = useCallback(async (providedToken) => { + const activeToken = providedToken ?? token; + if (!activeToken) { + setUser(null); + return null; + } + + const userInfo = await fetchCurrentUser(activeToken); + if (!userInfo) { + localStorage.removeItem(TOKEN_KEY); + setToken(null); + setUser(null); + return null; + } + + if (!token) { + setToken(activeToken); + } + setUser(userInfo); + return userInfo; + }, [token]); + useEffect(() => { const stored = localStorage.getItem(TOKEN_KEY); if (!stored) { @@ -26,17 +48,14 @@ export function AuthProvider({ children }) { return; } - fetchCurrentUser(stored) - .then((data) => { - if (data) { - setToken(stored); - setUser(data); - } - - else { - localStorage.removeItem(TOKEN_KEY); - } - }).catch(() => localStorage.removeItem(TOKEN_KEY)).finally(() => setLoading(false));}, []); + refreshUser(stored) + .catch(() => { + localStorage.removeItem(TOKEN_KEY); + setToken(null); + setUser(null); + }) + .finally(() => setLoading(false)); + }, [refreshUser]); const login = useCallback(async (username, password) => { const res = await fetch("/api/v1/auth/login", { @@ -55,12 +74,10 @@ export function AuthProvider({ children }) { localStorage.setItem(TOKEN_KEY, jwt); setToken(jwt); - const userInfo = await fetchCurrentUser(jwt); - - setUser(userInfo); + const userInfo = await refreshUser(jwt); return userInfo; - }, []); + }, [refreshUser]); const register = useCallback(async ({ username, password, email, fullName, phone }) => { const res = await fetch("/api/v1/auth/register", { @@ -79,11 +96,10 @@ export function AuthProvider({ children }) { localStorage.setItem(TOKEN_KEY, jwt); setToken(jwt); - const userInfo = await fetchCurrentUser(jwt); - setUser(userInfo); + const userInfo = await refreshUser(jwt); return userInfo; - }, []); + }, [refreshUser]); const logout = useCallback(() => { localStorage.removeItem(TOKEN_KEY); @@ -91,7 +107,7 @@ export function AuthProvider({ children }) { setUser(null);}, []); return ( - + {children} ); -- 2.49.1 From 781eb48ca9b73bfccc10367810b27aab90eafc13 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:08:00 -0600 Subject: [PATCH 3/5] Fix item loading --- web/app/adopt/page.js | 30 +++++++++++++++++++----------- web/app/products/page.js | 32 +++++++++++++++++--------------- web/components/PetCard.js | 10 +++++++++- web/components/PetProfile.js | 10 +++++++++- web/components/ProductCard.js | 10 +++++++++- web/components/ProductProfile.js | 10 +++++++++- web/lib/fetchAllPages.js | 19 +++++++++++++++++++ 7 files changed, 91 insertions(+), 30 deletions(-) create mode 100644 web/lib/fetchAllPages.js diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index 90293d8c..97fe1472 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import PetCard from "@/components/PetCard"; +import { fetchAllPages } from "@/lib/fetchAllPages"; const API_BASE = ""; @@ -13,7 +14,7 @@ export default function AdoptPage() { const [search, setSearch] = useState(""); const [query, setQuery] = useState(""); - const PAGE_SIZE = 200; + const PAGE_SIZE = 100; useEffect(() => { fetch(`${API_BASE}/api/v1/health`) @@ -22,16 +23,23 @@ export default function AdoptPage() { }, []); useEffect(() => { - const params = new URLSearchParams({ page: 0, size: PAGE_SIZE, sort: "petId,asc" }); - if (query) params.set("q", query); + setLoading(true); + setError(null); - fetch(`${API_BASE}/api/v1/pets?${params}`) - .then((res) => { - if (!res.ok) throw new Error(`HTTP ${res.status} – ${res.statusText}`); - return res.json(); - }) - .then((data) => { - setPets(data.content ?? []); + fetchAllPages((page) => { + const params = new URLSearchParams({ + page: String(page), + size: String(PAGE_SIZE), + sort: "petId,asc", + status: "Available", + }); + if (query) { + params.set("q", query); + } + return `${API_BASE}/api/v1/pets?${params}`; + }) + .then((allPets) => { + setPets(allPets); }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); @@ -58,7 +66,7 @@ export default function AdoptPage() { setSearch(e.target.value)} /> diff --git a/web/app/products/page.js b/web/app/products/page.js index b3416326..19cf8670 100644 --- a/web/app/products/page.js +++ b/web/app/products/page.js @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import ProductCard from "@/components/ProductCard"; +import { fetchAllPages } from "@/lib/fetchAllPages"; const API_BASE = ""; @@ -12,24 +13,25 @@ export default function ProductsPage() { const [search, setSearch] = useState(""); const [query, setQuery] = useState(""); - const PAGE_SIZE = 200; + const PAGE_SIZE = 100; useEffect(() => { - const params = new URLSearchParams({ page: 0, size: PAGE_SIZE, sort: "prodId,asc" }); - if (query) { - params.set("q", query); - } + setLoading(true); + setError(null); - fetch(`${API_BASE}/api/v1/products?${params}`) - .then((res) => { - if (!res.ok) { - throw new Error(`HTTP ${res.status} – ${res.statusText}`); - } - - return res.json(); - }) - .then((data) => { - setProducts(data.content ?? []); + fetchAllPages((page) => { + const params = new URLSearchParams({ + page: String(page), + size: String(PAGE_SIZE), + sort: "prodId,asc", + }); + if (query) { + params.set("q", query); + } + return `${API_BASE}/api/v1/products?${params}`; + }) + .then((allProducts) => { + setProducts(allProducts); }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); diff --git a/web/components/PetCard.js b/web/components/PetCard.js index 4370bca1..4c6410b9 100644 --- a/web/components/PetCard.js +++ b/web/components/PetCard.js @@ -5,7 +5,15 @@ export default function PetCard({petId, petName, petSpecies, petStatus, imageUrl return (
- {petName} + {petName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />

{petName}

diff --git a/web/components/PetProfile.js b/web/components/PetProfile.js index a00732a4..8b7fd6f0 100644 --- a/web/components/PetProfile.js +++ b/web/components/PetProfile.js @@ -5,7 +5,15 @@ export default function PetProfile({ petId, petName, petSpecies, petBreed, petAg return (
- {petName} + {petName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />
diff --git a/web/components/ProductCard.js b/web/components/ProductCard.js index e27abbbd..0b4c76ed 100644 --- a/web/components/ProductCard.js +++ b/web/components/ProductCard.js @@ -4,7 +4,15 @@ export default function ProductCard({ prodId, prodName, categoryName, prodPrice, return (
- {prodName} + {prodName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />

{prodName}

diff --git a/web/components/ProductProfile.js b/web/components/ProductProfile.js index b8d0a9b2..fda41420 100644 --- a/web/components/ProductProfile.js +++ b/web/components/ProductProfile.js @@ -4,7 +4,15 @@ export default function ProductProfile({ prodName, categoryName, prodDesc, prodP return (
- {prodName} + {prodName} { + e.currentTarget.onerror = null; + e.currentTarget.src = "/images/pet-placeholder.png"; + }} + />
diff --git a/web/lib/fetchAllPages.js b/web/lib/fetchAllPages.js new file mode 100644 index 00000000..624d172c --- /dev/null +++ b/web/lib/fetchAllPages.js @@ -0,0 +1,19 @@ +export async function fetchAllPages(urlBuilder) { + const items = []; + let page = 0; + let totalPages = 1; + + while (page < totalPages) { + const res = await fetch(urlBuilder(page)); + if (!res.ok) { + throw new Error(`HTTP ${res.status} – ${res.statusText}`); + } + + const data = await res.json(); + items.push(...(data.content ?? [])); + totalPages = Math.max(data.totalPages ?? 1, 1); + page += 1; + } + + return items; +} -- 2.49.1 From 99855a6e99907ef827aa4385ee580644c01ccdec Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:11:53 -0600 Subject: [PATCH 4/5] Fix pet sorting --- web/app/adopt/page.js | 2 +- web/app/appointments/page.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index 97fe1472..a6a2784a 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -30,7 +30,7 @@ export default function AdoptPage() { const params = new URLSearchParams({ page: String(page), size: String(PAGE_SIZE), - sort: "petId,asc", + sort: "id,asc", status: "Available", }); if (query) { diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index fac5a86f..676b953e 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -249,7 +249,7 @@ function AppointmentsPage() { .then((data) => setServices(data.content ?? [])) .catch(() => {}); - fetch(`${API_BASE}/api/v1/pets?size=200&sort=petId,asc`) + fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,asc&status=Available`) .then((r) => r.json()) .then((data) => setAllPets(data.content ?? [])) .catch(() => {}); -- 2.49.1 From 63162487b540aaa08b312171d912b1192f2c9639 Mon Sep 17 00:00:00 2001 From: augmentedpotato Date: Thu, 2 Apr 2026 09:32:42 -0600 Subject: [PATCH 5/5] Fix profile images --- web/app/appointments/page.js | 10 +---- web/app/profile/page.js | 75 +++++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 19 deletions(-) diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 676b953e..4643bf0e 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -615,15 +615,7 @@ function AppointmentsPage() { {submitting ? "Booking..." : isAdoptionService ? "Schedule Adoption Visit" : "Book Appointment"} - ) : ( -
-

Appointment Booking

-
-

Web appointment booking is currently available for customer accounts only.

-

Admin and staff accounts can still review appointment activity below.

-
-
- )} + ) : null}

{canBookAppointments ? "Your Appointments" : "Appointments"}

diff --git a/web/app/profile/page.js b/web/app/profile/page.js index d0b78c5e..ff79521e 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -1,6 +1,6 @@ "use client"; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -9,6 +9,7 @@ const API_BASE = ""; export default function ProfilePage() { const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); + const petImageObjectUrlsRef = useRef([]); const [pets, setPets] = useState([]); const [loadingPets, setLoadingPets] = useState(false); @@ -25,6 +26,13 @@ export default function ProfilePage() { const [profileSuccess, setProfileSuccess] = useState(null); const [avatarSubmitting, setAvatarSubmitting] = useState(false); + const clearPetImageObjectUrls = useCallback(() => { + for (const objectUrl of petImageObjectUrlsRef.current) { + URL.revokeObjectURL(objectUrl); + } + petImageObjectUrlsRef.current = []; + }, []); + useEffect(() => { if (!loading && !user) { router.replace(`/login?next=${encodeURIComponent("/profile")}`); @@ -40,17 +48,64 @@ export default function ProfilePage() { }); }, [user]); - const loadPets = useCallback(() => { + const loadPets = useCallback(async () => { if (!token) return; setLoadingPets(true); - fetch(`${API_BASE}/api/v1/my-pets`, { - headers: { Authorization: `Bearer ${token}` }, - }) - .then((r) => r.json()) - .then(setPets) - .catch(() => {}) - .finally(() => setLoadingPets(false)); - }, [token]); + + try { + const response = await fetch(`${API_BASE}/api/v1/my-pets`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!response.ok) { + throw new Error(`Request failed (${response.status})`); + } + + const petData = await response.json(); + clearPetImageObjectUrls(); + + const petsWithResolvedImages = await Promise.all( + (Array.isArray(petData) ? petData : []).map(async (pet) => { + if (!pet.imageUrl) { + return pet; + } + + try { + const imageResponse = await fetch(`${API_BASE}${pet.imageUrl}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + if (!imageResponse.ok) { + return { ...pet, imageUrl: null }; + } + + const blob = await imageResponse.blob(); + const objectUrl = URL.createObjectURL(blob); + petImageObjectUrlsRef.current.push(objectUrl); + + return { ...pet, imageUrl: objectUrl }; + } catch { + return { ...pet, imageUrl: null }; + } + }) + ); + + setPets(petsWithResolvedImages); + } + + catch { + } + + finally { + setLoadingPets(false); + } + }, [token, clearPetImageObjectUrls]); + + useEffect(() => { + return () => { + clearPetImageObjectUrls(); + }; + }, [clearPetImageObjectUrls]); useEffect(() => { if (user?.role === "CUSTOMER") { -- 2.49.1