From 3111553009860b3f372151e46671928053295c76 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 00:38:04 -0600 Subject: [PATCH 1/3] web fixes --- web/app/appointments/page.js | 4 ++-- web/app/cart/page.js | 3 +++ web/app/chat/page.js | 4 ++-- web/app/contact/page.js | 4 ++-- web/app/register/page.js | 3 +++ web/components/Navigation.js | 7 ++----- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 67170c59..78fd3d89 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -425,12 +425,12 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN"; }) .then((r) => r.json()) .then(setStores) - .catch(() => {}); + .catch(() => setError("Failed to load stores.")); fetch(`${API_BASE}/api/v1/services?size=100`) .then((r) => r.json()) .then((data) => setServices(data.content ?? [])) - .catch(() => {}); + .catch(() => setError("Failed to load services.")); fetch(`${API_BASE}/api/v1/pets?size=200&sort=id,asc&status=Available`) .then((r) => r.json()) diff --git a/web/app/cart/page.js b/web/app/cart/page.js index e555a5b2..344e50e7 100644 --- a/web/app/cart/page.js +++ b/web/app/cart/page.js @@ -43,6 +43,9 @@ function PaymentForm({ clientSecret, totalAmount, onSuccess, onCancel }) { try { await apiCompleteCheckout(token, paymentIntentId); } catch { + setPayError("Order confirmation failed. Please contact support."); + setPaying(false); + return; } onSuccess(); } diff --git a/web/app/chat/page.js b/web/app/chat/page.js index e54ea966..4d8d9314 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -57,7 +57,7 @@ function ChatPage() { } catch { - //Silent fail + setError("Failed to load messages."); } }, [token]); @@ -139,7 +139,7 @@ function ChatPage() { if (open) convId = open.id; } } catch { - // + setError("Failed to load conversations."); } } diff --git a/web/app/contact/page.js b/web/app/contact/page.js index 19295ebd..fa5c04ae 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -30,8 +30,8 @@ export default function ContactPage() {

General Contact

-

Email: support@petshop.com

-

Phone: (000) 000-0000

+

Email: hello@leonspetstore.com.au

+

Phone: (03) 9000 0000

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

diff --git a/web/app/register/page.js b/web/app/register/page.js index 674e4b9e..a5cd5b10 100644 --- a/web/app/register/page.js +++ b/web/app/register/page.js @@ -135,6 +135,8 @@ function RegisterPage() { value={form.phone} onChange={handleChange} required + pattern="[0-9\-\+\(\) ]{7,15}" + title="Enter a valid phone number" /> @@ -161,6 +163,7 @@ function RegisterPage() { value={form.confirmPassword} onChange={handleChange} required + minLength={6} autoComplete="new-password" /> diff --git a/web/components/Navigation.js b/web/components/Navigation.js index afd1ea50..39baa63a 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -15,14 +15,11 @@ export default function DisplayNav() { const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { - if (!token) return; - fetch("/api/v1/stores?size=100", { - headers: { Authorization: `Bearer ${token}` }, - }) + fetch("/api/v1/stores?size=100") .then((r) => (r.ok ? r.json() : null)) .then((data) => { if (data) setStores(data.content ?? []); }) .catch(() => {}); - }, [token]); + }, []); function handleLogout() { logout(); -- 2.49.1 From f45330a451a304ffda93a69534c41904f2be126c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 00:44:07 -0600 Subject: [PATCH 2/3] web issue fixes --- web/app/appointments/page.js | 15 +++++++++++++-- web/app/chat/page.js | 3 ++- web/app/contact/page.js | 11 ++++++++++- web/app/globals.css | 6 ------ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 78fd3d89..1733fb45 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -2,6 +2,7 @@ import dynamic from "next/dynamic"; import { useState, useEffect, useCallback, useRef } from "react"; +import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/context/AuthContext"; @@ -339,6 +340,7 @@ function AppointmentsPage() { const adoptionStoreName = searchParams.get("storeName") || ""; const didPreselectRef = useRef(false); + const errorRef = useRef(null); // Adoption-mode URL verification const [adoptionVerified, setAdoptionVerified] = useState(!adoptionMode); @@ -364,6 +366,12 @@ function AppointmentsPage() { const [error, setError] = useState(null); const [success, setSuccess] = useState(null); + useEffect(() => { + if (error && errorRef.current) { + errorRef.current.scrollIntoView({ behavior: "smooth", block: "center" }); + } + }, [error]); + const [appointments, setAppointments] = useState([]); const [loadingAppointments, setLoadingAppointments] = useState(false); @@ -779,7 +787,7 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";

{adoptionMode ? "New Adoption" : "New Appointment"}

- {error &&
{error}
} + {error &&
{error}
} {adoptionMode && adoptionVerifyLoading && (

Verifying pet details…

@@ -805,7 +813,10 @@ const canBookAppointments = user?.role === "CUSTOMER" || user?.role === "ADMIN";
Select Your Pet {eligiblePets.length === 0 ? ( -

You have no adopted pets available for appointments.

+

+ You have no adopted pets available.{" "} + Add a pet on your profile page. +

) : (
{eligiblePets.map((p) => ( diff --git a/web/app/chat/page.js b/web/app/chat/page.js index 4d8d9314..f49be420 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -265,6 +265,7 @@ function ChatPage() { const isHuman = conversation?.mode === "HUMAN"; const hasStaff = !!conversation?.staffId; const isClosed = conversation?.status === "CLOSED"; + const hasStaffMessage = messages.some((m) => m.senderId !== user?.id); const staffStatusLabel = isClosed ? "Conversation closed" @@ -322,7 +323,7 @@ function ChatPage() {
- {!hasStaff && !isClosed && ( + {!hasStaff && !hasStaffMessage && !isClosed && (
A support agent will be with you shortly. You can send messages while you wait. diff --git a/web/app/contact/page.js b/web/app/contact/page.js index fa5c04ae..f5da828f 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -2,6 +2,15 @@ import { useState, useEffect } from "react"; +function getStoreImage(store) { + if (store.imageUrl) return store.imageUrl; + const name = store.storeName?.toLowerCase() ?? ""; + if (name.includes("downtown")) return "/stores/downtown.webp"; + if (name.includes("north")) return "/stores/north.webp"; + if (name.includes("west")) return "/stores/west.webp"; + return "/images/pet-placeholder.png"; +} + export default function ContactPage() { const [locations, setLocations] = useState([]); const [loading, setLoading] = useState(true); @@ -52,7 +61,7 @@ export default function ContactPage() {
{location.storeName} { diff --git a/web/app/globals.css b/web/app/globals.css index 2dbbe14a..71632aae 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -12,12 +12,6 @@ --font-mono: var(--font-geist-mono); } -@media (prefers-color-scheme: dark) { - :root { - --background: #0a0a0a; - --foreground: #ededed; - } -} body { background: var(--background); -- 2.49.1 From 0e1eb056a435e83761f90d0a981b2c2e11187766 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 00:46:32 -0600 Subject: [PATCH 3/3] contact form with email --- .../backend/controller/ContactController.java | 40 +++++++++++ .../petshop/backend/service/EmailService.java | 13 ++++ web/app/contact/page.js | 67 +++++++++++++++++++ web/app/globals.css | 10 +++ 4 files changed, 130 insertions(+) create mode 100644 backend/src/main/java/com/petshop/backend/controller/ContactController.java diff --git a/backend/src/main/java/com/petshop/backend/controller/ContactController.java b/backend/src/main/java/com/petshop/backend/controller/ContactController.java new file mode 100644 index 00000000..c6b4cf1d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ContactController.java @@ -0,0 +1,40 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.EmailService; +import com.petshop.backend.util.AuthenticationHelper; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/contact") +public class ContactController { + + private final EmailService emailService; + private final UserRepository userRepository; + + public ContactController(EmailService emailService, UserRepository userRepository) { + this.emailService = emailService; + this.userRepository = userRepository; + } + + public record ContactRequest( + @NotBlank @Size(max = 150) String subject, + @NotBlank @Size(max = 2000) String body + ) {} + + @PostMapping + public ResponseEntity sendContactEmail(@Valid @RequestBody ContactRequest req) { + Long userId = AuthenticationHelper.getAuthenticatedUserId(); + User user = userRepository.findById(userId).orElseThrow(); + emailService.sendContactMessage(user, req.subject(), req.body()); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/EmailService.java b/backend/src/main/java/com/petshop/backend/service/EmailService.java index 667aad08..b944c053 100644 --- a/backend/src/main/java/com/petshop/backend/service/EmailService.java +++ b/backend/src/main/java/com/petshop/backend/service/EmailService.java @@ -129,6 +129,19 @@ public class EmailService { } } + public void sendContactMessage(User user, String subject, String body) { + if (user.getEmail() == null || user.getEmail().isBlank()) return; + String html = """ +
+

Contact form message

+

From: %s (%s)

+

Subject: %s

+
+

%s

+
""".formatted(esc(firstName(user)), esc(user.getEmail()), esc(subject), esc(body)); + send(user.getId(), user.getEmail(), "Contact: " + subject, html); + } + public void sendChatTranscript(Conversation conversation, List messages, User customer) { if (customer == null || customer.getEmail() == null || customer.getEmail().isBlank()) return; String subject = "Your PetShop support transcript"; diff --git a/web/app/contact/page.js b/web/app/contact/page.js index f5da828f..79f9b555 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { useAuth } from "@/context/AuthContext"; function getStoreImage(store) { if (store.imageUrl) return store.imageUrl; @@ -12,10 +13,17 @@ function getStoreImage(store) { } export default function ContactPage() { + const { token } = useAuth(); const [locations, setLocations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [subject, setSubject] = useState(""); + const [body, setBody] = useState(""); + const [sending, setSending] = useState(false); + const [sendError, setSendError] = useState(null); + const [sendSuccess, setSendSuccess] = useState(false); + useEffect(() => { const params = new URLSearchParams({ page: "0", size: "100", sort: "storeName,asc" }); fetch(`/api/v1/stores?${params}`) @@ -28,6 +36,27 @@ export default function ContactPage() { .finally(() => setLoading(false)); }, []); + async function handleSend(e) { + e.preventDefault(); + setSending(true); + setSendError(null); + try { + const res = await fetch("/api/v1/contact", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ subject, body }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setSendSuccess(true); + setSubject(""); + setBody(""); + } catch (err) { + setSendError("Failed to send message. Please try again."); + } finally { + setSending(false); + } + } + return (
@@ -44,6 +73,44 @@ export default function ContactPage() {

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

+ {token && ( +
+

Send Us a Message

+ {sendSuccess ? ( +

Your message has been sent. We'll be in touch soon.

+ ) : ( + + +