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..a6a2784a 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 = ""; @@ -12,10 +13,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 = 100; useEffect(() => { fetch(`${API_BASE}/api/v1/health`) @@ -27,25 +26,29 @@ export default function AdoptPage() { setLoading(true); setError(null); - const params = new URLSearchParams({ page, size: PAGE_SIZE, sort: "id,asc" }); - if (query) params.set("q", query); - - 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 ?? []); - setTotalPages(data.totalPages ?? 0); + fetchAllPages((page) => { + const params = new URLSearchParams({ + page: String(page), + size: String(PAGE_SIZE), + sort: "id,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)); - }, [page, query]); + }, [query]); function handleSearch(e) { e.preventDefault(); - setPage(0); + setLoading(true); + setError(null); setQuery(search.trim()); } @@ -63,7 +66,7 @@ export default function AdoptPage() { setSearch(e.target.value)} /> @@ -72,7 +75,7 @@ export default function AdoptPage() { @@ -125,25 +128,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..4643bf0e 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 ? ( - + ) : ( - ))} - - )} + Date + - )} - {serviceId && ( -
- {petSectionLabel} - {petsToShow.length === 0 ? ( -

{noPetsMessage}

- ) : isAdoptionService ? ( -
- {petsToShow.map((p) => ( - - ))} -
- ) : ( -
- {petsToShow.map((p) => ( - - ))} -
- )} -
- )} + {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) => ( + + ))} +
+ )} +
+ )} + + + + ) : null}
-

Your Appointments

+

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

{loadingAppointments ? (

Loading appointments...

) : appointments.length === 0 ? ( @@ -637,4 +655,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..8571bfeb 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; @@ -1008,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 { @@ -1037,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; @@ -1698,4 +1804,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..2e7d1fe1 100644 --- a/web/app/login/page.js +++ b/web/app/login/page.js @@ -1,12 +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(""); @@ -20,7 +33,7 @@ export default function LoginPage() { try { await login(username, password); - router.push("/"); + router.push(resolveNextPath(searchParams.get("next"))); } catch (err) { @@ -64,12 +77,16 @@ export default function LoginPage() { {loading ? "Logging in…" : "Log In"} - +

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

); } + +export default dynamic(() => Promise.resolve(LoginPage), { + ssr: false, +}); 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..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 = ""; @@ -11,39 +12,35 @@ 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 = 100; useEffect(() => { setLoading(true); setError(null); - const params = new URLSearchParams({ page, size: PAGE_SIZE, sort: "prodId,asc" }); - if (query) { - params.set("q", query); - } - - 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 ?? []); - setTotalPages(data.totalPages ?? 0); + 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)); - }, [page, query]); + }, [query]); function handleSearch(e) { e.preventDefault(); - setPage(0); + setLoading(true); + setError(null); setQuery(search.trim()); } @@ -70,7 +67,7 @@ export default function ProductsPage() { @@ -108,25 +105,6 @@ export default function ProductsPage() { )} - {!loading && totalPages > 1 && ( -
- - Page {page + 1} of {totalPages} - -
- )} ); diff --git a/web/app/profile/page.js b/web/app/profile/page.js index 6b28aa35..ff79521e 100644 --- a/web/app/profile/page.js +++ b/web/app/profile/page.js @@ -1,14 +1,15 @@ "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"; const API_BASE = ""; export default function ProfilePage() { - const {user, token, loading, logout} = useAuth(); + const {user, token, loading, logout, refreshUser} = useAuth(); const router = useRouter(); + const petImageObjectUrlsRef = useRef([]); const [pets, setPets] = useState([]); const [loadingPets, setLoadingPets] = useState(false); @@ -19,25 +20,92 @@ 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); + + const clearPetImageObjectUrls = useCallback(() => { + for (const objectUrl of petImageObjectUrlsRef.current) { + URL.revokeObjectURL(objectUrl); + } + petImageObjectUrlsRef.current = []; + }, []); useEffect(() => { if (!loading && !user) { - router.replace("/login"); + router.replace(`/login?next=${encodeURIComponent("/profile")}`); } }, [user, loading, router]); - const loadPets = useCallback(() => { + useEffect(() => { + setProfileForm({ + fullName: user?.fullName || "", + email: user?.email || "", + phone: user?.phone || "", + }); + }, [user]); + + 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") { @@ -50,6 +118,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 +337,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 +356,65 @@ export default function ProfilePage() { ))} - + )} +
+ + + + @@ -194,7 +423,7 @@ export default function ProfilePage() {

My Pets

- +
{showForm && ( @@ -285,11 +514,11 @@ export default function ProfilePage() { {pet.breed && {pet.breed}}
- - -
- - ))} + + + + + ))} )} diff --git a/web/app/register/page.js b/web/app/register/page.js index 20dcd2c1..17f49672 100644 --- a/web/app/register/page.js +++ b/web/app/register/page.js @@ -1,12 +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: "", @@ -40,7 +53,7 @@ export default function RegisterPage() { phone: form.phone, password: form.password, }); - router.push("/"); + router.push(resolveNextPath(searchParams.get("next"))); } catch (err) { @@ -140,12 +153,16 @@ export default function RegisterPage() { {loading ? "Creating account…" : "Register"} - +

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

); } + +export default dynamic(() => Promise.resolve(RegisterPage), { + ssr: false, +}); 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 +} 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/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} ); 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; +}