From 7ce22c48015ec12530b6ae9f0e9947596416eb76 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 22:37:51 -0600 Subject: [PATCH 1/8] fix images and empty space --- web/app/ai-chat/page.js | 71 +++++++++++++++++++++++++++++++++++------ web/app/chat/page.js | 71 +++++++++++++++++++++++++++++++++++------ web/app/globals.css | 2 +- 3 files changed, 123 insertions(+), 21 deletions(-) diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index 09ac5ba3..ce8895b8 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -8,6 +8,66 @@ import { createStompClient } from "@/lib/chatSocket"; const API_BASE = ""; +function isImageFilename(name) { + return /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(name || ""); +} + +function AttachmentPreview({ url, name, token }) { + const [blobUrl, setBlobUrl] = useState(null); + const isImage = isImageFilename(name); + + useEffect(() => { + if (!url || !token) return; + let objectUrl; + fetch(url, { headers: { Authorization: `Bearer ${token}` } }) + .then((r) => (r.ok ? r.blob() : null)) + .then((blob) => { + if (blob) { + objectUrl = URL.createObjectURL(blob); + setBlobUrl(objectUrl); + } + }) + .catch(() => {}); + return () => { if (objectUrl) URL.revokeObjectURL(objectUrl); }; + }, [url, token]); + + if (isImage) { + return ( +
+ {blobUrl ? ( + {name window.open(blobUrl, "_blank")} + /> + ) : ( + 📎 Loading image… + )} +
+ ); + } + + return ( +
+ { + e.preventDefault(); + if (!blobUrl) return; + const a = document.createElement("a"); + a.href = blobUrl; + a.download = name || "attachment"; + a.click(); + }} + > + 📎 {name || "Attachment"} + +
+ ); +} + function AiChatPage() { const { user, token, loading: authLoading } = useAuth(); const router = useRouter(); @@ -501,16 +561,7 @@ function AiChatPage() { ))} {msg.attachmentUrl && ( -
- - 📎 {msg.attachmentName || "Attachment"} - -
+ )}
{msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""} diff --git a/web/app/chat/page.js b/web/app/chat/page.js index f5b8432f..1957fe78 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -8,6 +8,66 @@ import { createStompClient } from "@/lib/chatSocket"; const API_BASE = ""; +function isImageFilename(name) { + return /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(name || ""); +} + +function AttachmentPreview({ url, name, token }) { + const [blobUrl, setBlobUrl] = useState(null); + const isImage = isImageFilename(name); + + useEffect(() => { + if (!url || !token) return; + let objectUrl; + fetch(url, { headers: { Authorization: `Bearer ${token}` } }) + .then((r) => (r.ok ? r.blob() : null)) + .then((blob) => { + if (blob) { + objectUrl = URL.createObjectURL(blob); + setBlobUrl(objectUrl); + } + }) + .catch(() => {}); + return () => { if (objectUrl) URL.revokeObjectURL(objectUrl); }; + }, [url, token]); + + if (isImage) { + return ( +
+ {blobUrl ? ( + {name window.open(blobUrl, "_blank")} + /> + ) : ( + 📎 Loading image… + )} +
+ ); + } + + return ( +
+ { + e.preventDefault(); + if (!blobUrl) return; + const a = document.createElement("a"); + a.href = blobUrl; + a.download = name || "attachment"; + a.click(); + }} + > + 📎 {name || "Attachment"} + +
+ ); +} + function ChatPage() { const { user, token, loading: authLoading } = useAuth(); const router = useRouter(); @@ -532,16 +592,7 @@ function ChatPage() { ))} {msg.attachmentUrl && ( -
- - 📎 {msg.attachmentName || "Attachment"} - -
+ )}
{msg.timestamp ? new Date(msg.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : ""} diff --git a/web/app/globals.css b/web/app/globals.css index f54ed79b..eab50e73 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -683,8 +683,8 @@ body { } .info-page { - min-height: 100vh; background: linear-gradient(to bottom, #f9f9f9, #ffffff); + padding-bottom: 4rem; } .info-hero { -- 2.49.1 From 3ff7df6f883243ef52badfe7c574e11a83fe7576 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 22:46:49 -0600 Subject: [PATCH 2/8] fix chat escalation and sidebar --- web/app/ai-chat/page.js | 166 ++++++++++++++++++++++++++++++++++------ web/app/chat/page.js | 65 +++++++++++++--- 2 files changed, 195 insertions(+), 36 deletions(-) diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index ce8895b8..c059e3f4 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -82,6 +82,7 @@ function AiChatPage() { const [error, setError] = useState(null); const [conversations, setConversations] = useState([]); const [convsLoading, setConvsLoading] = useState(false); + const [closedExpanded, setClosedExpanded] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const messagesEndRef = useRef(null); @@ -418,12 +419,28 @@ function AiChatPage() { method: "POST", headers: { Authorization: `Bearer ${token}` }, }); - router.push(`/chat?id=${conversation.id}`); + setConversation((prev) => prev ? { ...prev, mode: "HUMAN" } : prev); } catch { setError("Could not connect to live support. Please try again."); } } + async function handleCloseConversation() { + if (!conversation || conversation.status === "CLOSED") return; + try { + const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${conversation.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ status: "CLOSED" }), + }); + if (!res.ok) return; + const updated = await res.json(); + setConversation(updated); + await fetchConversations(); + } catch { + } + } + if (authLoading || loadingConv) { return (
@@ -436,6 +453,8 @@ function AiChatPage() { const isEscalated = conversation?.mode === "HUMAN"; const isClosed = conversation?.status === "CLOSED"; + const hasStaff = !!conversation?.staffId; + const hasStaffMessage = messages.some((m) => m.senderId !== user?.id); return (
@@ -456,7 +475,7 @@ function AiChatPage() {

No conversations yet.

)}
- {conversations.map((conv) => ( + {conversations.filter(c => c.status !== "CLOSED").map((conv) => ( ))} + {conversations.some(c => c.status === "CLOSED") && ( + <> + + {closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => ( + + ))} + + )}
)} - + {isEscalated && !isClosed && ( + + )}
+ {isEscalated && !hasStaff && !hasStaffMessage && !isClosed && ( +
+ + A support agent will be with you shortly. You can send messages while you wait. +
+ )} +
{messages.length === 0 && (
-
🐾
+
{isEscalated ? "💬" : "🐾"}

- Hello{user.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}! I'm your pet care assistant. - Ask me about pet recommendations, care tips, supplies, or anything pet-related! + {isEscalated ? "Your conversation has started. A support agent will join soon." : `Hello${user.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}! I'm your pet care assistant. Ask me about pet recommendations, care tips, supplies, or anything pet-related!`}

)} @@ -547,7 +594,7 @@ function AiChatPage() { ...(isOwn ? s.messageRowUser : s.messageRowAgent), }} > - {!isOwn &&
🐾
} + {!isOwn &&
{isEscalated ? "👤" : "🐾"}
}
No conversations yet.

)}
- {conversations.map((conv) => ( + {conversations.filter(c => c.status !== "CLOSED").map((conv) => ( ))} + {conversations.some(c => c.status === "CLOSED") && ( + <> + + {closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => ( + + ))} + + )}
)} - + {!isHuman && ( + + )}
@@ -1145,6 +1173,21 @@ const s = { }, convStatusOpen: { background: "#e6f9ee", color: "#1a7a3c" }, convStatusClosed: { background: "#f0f0f0", color: "#888" }, + convItemClosed: { opacity: 0.7 }, + closedSectionToggle: { + width: "100%", + background: "#f5f5f5", + border: "none", + borderTop: "1px solid #e8e8e8", + padding: "0.5rem 1rem", + fontSize: "0.78rem", + fontWeight: 600, + color: "#666", + cursor: "pointer", + display: "flex", + justifyContent: "space-between", + alignItems: "center", + }, newConvSidebarBtn: { margin: "0.65rem 1rem", background: "#333", -- 2.49.1 From 2fecdca91757f9a285c035a986b564b06e0c509d Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 22:51:39 -0600 Subject: [PATCH 3/8] fix adopt search mobile layout --- web/app/globals.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/app/globals.css b/web/app/globals.css index eab50e73..7ae1c69d 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -3048,13 +3048,20 @@ body { width: 100%; } - .adopt-search-form { + .adopt-controls-row { flex-direction: column; align-items: stretch; } + .adopt-search-form { + flex-direction: column; + align-items: stretch; + width: 100%; + } + .adopt-search-input { max-width: 100%; + width: 100%; } .adopt-search-btn, -- 2.49.1 From 8a9e4e75b55ed5c40771bc0624b024a424d27bcf Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 22:59:18 -0600 Subject: [PATCH 4/8] fix about section spacing and text --- web/app/globals.css | 29 +++++++++++++++++------------ web/app/page.js | 4 ++-- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/web/app/globals.css b/web/app/globals.css index 7ae1c69d..29edace2 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -684,31 +684,36 @@ body { .info-page { background: linear-gradient(to bottom, #f9f9f9, #ffffff); - padding-bottom: 4rem; } .info-hero { text-align: center; - padding: 4rem 2rem 3rem; + padding: 2.5rem 2rem 1.5rem; } .info-title { - font-size: 3rem; + font-size: 1.6rem; color: #333; - margin-bottom: 1rem; + margin-bottom: 0.5rem; font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.08em; } .info-subtitle { - font-size: 1.25rem; - color: #666; - margin-bottom: 1.5rem; + font-size: 1rem; + color: #888; + margin-bottom: 1rem; + max-width: 520px; + margin-left: auto; + margin-right: auto; + line-height: 1.6; } .info-content { max-width: 1200px; margin: 0 auto; - padding: 0 2rem 4rem; + padding: 0 2rem 3rem; display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.5rem; @@ -3147,19 +3152,19 @@ img, video, iframe { @media (max-width: 768px) { .info-title { - font-size: 2rem; + font-size: 1.3rem; } .info-subtitle { - font-size: 1.1rem; + font-size: 0.95rem; } } @media (max-width: 480px) { .info-title { - font-size: 1.6rem; + font-size: 1.2rem; } .info-subtitle { - font-size: 1rem; + font-size: 0.9rem; } .image-links-container { grid-template-columns: 1fr; diff --git a/web/app/page.js b/web/app/page.js index 8b928be1..610ed281 100644 --- a/web/app/page.js +++ b/web/app/page.js @@ -79,8 +79,8 @@ export default function Home() { {/* About Us */}
-

About Leon's Pet Store

-

Your trusted local destination for pet care, adoption, and supplies — built on a love for animals and community.

+

About Us

+

A full-service pet store built on a love for animals and community.

-- 2.49.1 From c0be2a6903bf44091a13d11e316824067d416268 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 23:03:54 -0600 Subject: [PATCH 5/8] pet owner search --- .../java/com/petshop/backend/repository/PetRepository.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java index 8b1aa178..d301be55 100644 --- a/backend/src/main/java/com/petshop/backend/repository/PetRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/PetRepository.java @@ -43,8 +43,8 @@ public interface PetRepository extends JpaRepository { @Query("SELECT p FROM Pet p WHERE p.id = :id") Optional findByIdForUpdate(@Param("id") Long id); - @Query("SELECT p FROM Pet p WHERE " + - "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + + @Query("SELECT p FROM Pet p LEFT JOIN p.owner o WHERE " + + "(:q IS NULL OR LOWER(p.petName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(p.petSpecies) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.petBreed, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(o.firstName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(o.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(CONCAT(COALESCE(o.firstName, ''), ' ', COALESCE(o.lastName, ''))) LIKE LOWER(CONCAT('%', :q, '%'))) AND " + "(:species IS NULL OR LOWER(p.petSpecies) = LOWER(:species)) AND " + "(:breed IS NULL OR LOWER(COALESCE(p.petBreed, '')) = LOWER(:breed)) AND " + "(:status IS NULL OR LOWER(p.petStatus) = LOWER(:status)) AND " + -- 2.49.1 From 703402b5b6612ebddde653417984c651cea7ecbf Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 23:11:53 -0600 Subject: [PATCH 6/8] fix chat badge on reply --- .../org/example/petshopdesktop/api/ChatRealtimeClient.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java index d29d3aab..93f90a73 100644 --- a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java @@ -90,8 +90,9 @@ public class ChatRealtimeClient implements WebSocket.Listener { for (ConversationResponse conv : globalConversations.values()) { if ("CLOSED".equals(conv.getStatus())) continue; - // Needs pickup - if (conv.getHumanRequestedAt() != null && conv.getStaffId() == null) { + // Needs pickup - only if we haven't already replied + if (conv.getHumanRequestedAt() != null && conv.getStaffId() == null + && (currentUserId == null || !currentUserId.equals(conv.getLastSenderId()))) { return true; } -- 2.49.1 From e193e294992a52c3a13c42a97699db7db9dd0df9 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 23:12:23 -0600 Subject: [PATCH 7/8] fix contact layout and chat ui --- web/app/ai-chat/page.js | 65 ++++++++++++++++------------- web/app/chat/page.js | 64 +++++++++++++++------------- web/app/contact/page.js | 92 ++++++++++++++++++++--------------------- web/app/globals.css | 4 ++ 4 files changed, 119 insertions(+), 106 deletions(-) diff --git a/web/app/ai-chat/page.js b/web/app/ai-chat/page.js index c059e3f4..6dbf101a 100644 --- a/web/app/ai-chat/page.js +++ b/web/app/ai-chat/page.js @@ -92,6 +92,7 @@ function AiChatPage() { const lastMessageIdRef = useRef(null); const fileInputRef = useRef(null); const lastScrolledIdRef = useRef(null); + const initialLoadDoneRef = useRef(false); useEffect(() => { if (!authLoading && !user) { @@ -103,6 +104,7 @@ function AiChatPage() { if (messages.length === 0) return; const lastMsg = messages[messages.length - 1]; if (lastMsg.id === lastScrolledIdRef.current) return; + if (!initialLoadDoneRef.current) return; lastScrolledIdRef.current = lastMsg.id; const area = messagesAreaRef.current; if (!area) return; @@ -114,6 +116,7 @@ function AiChatPage() { const fetchMessages = useCallback(async (convId) => { if (!token || !convId) return; + initialLoadDoneRef.current = false; try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, { headers: { Authorization: `Bearer ${token}` }, @@ -122,10 +125,17 @@ function AiChatPage() { const data = await res.json(); if (Array.isArray(data)) { setMessages(data); - if (data.length > 0) lastMessageIdRef.current = data[data.length - 1].id; + if (data.length > 0) { + lastMessageIdRef.current = data[data.length - 1].id; + lastScrolledIdRef.current = data[data.length - 1].id; + } + setTimeout(() => { + const area = messagesAreaRef.current; + if (area) area.scrollTop = area.scrollHeight; + initialLoadDoneRef.current = true; + }, 50); } } catch { - // silent } }, [token]); @@ -491,34 +501,31 @@ function AiChatPage() {
))} - {conversations.some(c => c.status === "CLOSED") && ( - <> - - {closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => ( - - ))} - - )} + {conversations.some(c => c.status === "CLOSED") && ( + <> + + {closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => ( + + ))} + + )} diff --git a/web/app/chat/page.js b/web/app/chat/page.js index 9c8e48f8..dbee3ed3 100644 --- a/web/app/chat/page.js +++ b/web/app/chat/page.js @@ -92,6 +92,7 @@ function ChatPage() { const lastMessageIdRef = useRef(null); const fileInputRef = useRef(null); const lastScrolledIdRef = useRef(null); + const initialLoadDoneRef = useRef(false); useEffect(() => { if (!authLoading && !user) { @@ -103,6 +104,7 @@ function ChatPage() { if (messages.length === 0) return; const lastMsg = messages[messages.length - 1]; if (lastMsg.id === lastScrolledIdRef.current) return; + if (!initialLoadDoneRef.current) return; lastScrolledIdRef.current = lastMsg.id; const area = messagesAreaRef.current; if (!area) return; @@ -114,6 +116,7 @@ function ChatPage() { const fetchMessages = useCallback(async (convId) => { if (!token || !convId) return; + initialLoadDoneRef.current = false; try { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, { headers: { Authorization: `Bearer ${token}` }, @@ -128,10 +131,16 @@ function ChatPage() { if (data.length > 0) { lastMessageIdRef.current = data[data.length - 1].id; + lastScrolledIdRef.current = data[data.length - 1].id; } + setTimeout(() => { + const area = messagesAreaRef.current; + if (area) area.scrollTop = area.scrollHeight; + initialLoadDoneRef.current = true; + }, 50); } - } - + } + catch { setError("Failed to load messages."); } @@ -495,34 +504,31 @@ function ChatPage() { ))} - {conversations.some(c => c.status === "CLOSED") && ( - <> - - {closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => ( - - ))} - - )} + {conversations.some(c => c.status === "CLOSED") && ( + <> + + {closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => ( + + ))} + + )} diff --git a/web/app/contact/page.js b/web/app/contact/page.js index eb8274e9..5ae4b5e4 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -50,7 +50,7 @@ export default function ContactPage() { setSendSuccess(true); setSubject(""); setBody(""); - } catch (err) { + } catch { setSendError("Failed to send message. Please try again."); } finally { setSending(false); @@ -61,67 +61,63 @@ export default function ContactPage() {

Contact Us

-

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

+

Reach the team, find a location, or send us a message.

-
+
-

General Contact

+

Get in Touch

Email: hello@leonspetstore.com.au

Phone: (03) 9000 0000

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.

- ) : ( -
- -