fix contact layout and chat ui

This commit is contained in:
2026-04-15 23:12:23 -06:00
parent 73c4bc6cc7
commit 95b45c2e54
4 changed files with 119 additions and 106 deletions

View File

@@ -92,6 +92,7 @@ function AiChatPage() {
const lastMessageIdRef = useRef(null); const lastMessageIdRef = useRef(null);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const lastScrolledIdRef = useRef(null); const lastScrolledIdRef = useRef(null);
const initialLoadDoneRef = useRef(false);
useEffect(() => { useEffect(() => {
if (!authLoading && !user) { if (!authLoading && !user) {
@@ -103,6 +104,7 @@ function AiChatPage() {
if (messages.length === 0) return; if (messages.length === 0) return;
const lastMsg = messages[messages.length - 1]; const lastMsg = messages[messages.length - 1];
if (lastMsg.id === lastScrolledIdRef.current) return; if (lastMsg.id === lastScrolledIdRef.current) return;
if (!initialLoadDoneRef.current) return;
lastScrolledIdRef.current = lastMsg.id; lastScrolledIdRef.current = lastMsg.id;
const area = messagesAreaRef.current; const area = messagesAreaRef.current;
if (!area) return; if (!area) return;
@@ -114,6 +116,7 @@ function AiChatPage() {
const fetchMessages = useCallback(async (convId) => { const fetchMessages = useCallback(async (convId) => {
if (!token || !convId) return; if (!token || !convId) return;
initialLoadDoneRef.current = false;
try { try {
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
@@ -122,10 +125,17 @@ function AiChatPage() {
const data = await res.json(); const data = await res.json();
if (Array.isArray(data)) { if (Array.isArray(data)) {
setMessages(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 { } catch {
// silent
} }
}, [token]); }, [token]);
@@ -491,34 +501,31 @@ function AiChatPage() {
</div> </div>
</button> </button>
))} ))}
{conversations.some(c => c.status === "CLOSED") && (
<>
<button
style={s.closedSectionToggle}
onClick={() => setClosedExpanded(p => !p)}
>
<span>Closed ({conversations.filter(c => c.status === "CLOSED").length})</span>
<span>{closedExpanded ? "▲" : "▼"}</span>
</button>
{closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => (
<button
key={conv.id}
style={{ ...s.convItem, ...s.convItemClosed, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
onClick={() => switchConversation(conv.id)}
>
<div style={s.convItemTop}>
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
<span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span>
</div>
<div style={s.convItemBottom}>
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
</div>
</button>
))}
</>
)}
</div> </div>
{conversations.some(c => c.status === "CLOSED") && (
<>
<button style={s.closedSectionToggle} onClick={() => setClosedExpanded(p => !p)}>
<span>Closed ({conversations.filter(c => c.status === "CLOSED").length})</span>
<span>{closedExpanded ? "▲" : "▼"}</span>
</button>
{closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => (
<button
key={conv.id}
style={{ ...s.convItem, ...s.convItemClosed, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
onClick={() => switchConversation(conv.id)}
>
<div style={s.convItemTop}>
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
<span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span>
</div>
<div style={s.convItemBottom}>
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
</div>
</button>
))}
</>
)}
<button style={s.newConvSidebarBtn} onClick={handleNewConversation}> <button style={s.newConvSidebarBtn} onClick={handleNewConversation}>
+ New Conversation + New Conversation
</button> </button>

View File

@@ -92,6 +92,7 @@ function ChatPage() {
const lastMessageIdRef = useRef(null); const lastMessageIdRef = useRef(null);
const fileInputRef = useRef(null); const fileInputRef = useRef(null);
const lastScrolledIdRef = useRef(null); const lastScrolledIdRef = useRef(null);
const initialLoadDoneRef = useRef(false);
useEffect(() => { useEffect(() => {
if (!authLoading && !user) { if (!authLoading && !user) {
@@ -103,6 +104,7 @@ function ChatPage() {
if (messages.length === 0) return; if (messages.length === 0) return;
const lastMsg = messages[messages.length - 1]; const lastMsg = messages[messages.length - 1];
if (lastMsg.id === lastScrolledIdRef.current) return; if (lastMsg.id === lastScrolledIdRef.current) return;
if (!initialLoadDoneRef.current) return;
lastScrolledIdRef.current = lastMsg.id; lastScrolledIdRef.current = lastMsg.id;
const area = messagesAreaRef.current; const area = messagesAreaRef.current;
if (!area) return; if (!area) return;
@@ -114,6 +116,7 @@ function ChatPage() {
const fetchMessages = useCallback(async (convId) => { const fetchMessages = useCallback(async (convId) => {
if (!token || !convId) return; if (!token || !convId) return;
initialLoadDoneRef.current = false;
try { try {
const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, { const res = await fetch(`${API_BASE}/api/v1/chat/conversations/${convId}/messages`, {
headers: { Authorization: `Bearer ${token}` }, headers: { Authorization: `Bearer ${token}` },
@@ -128,10 +131,16 @@ function ChatPage() {
if (data.length > 0) { if (data.length > 0) {
lastMessageIdRef.current = data[data.length - 1].id; 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 { catch {
setError("Failed to load messages."); setError("Failed to load messages.");
} }
@@ -495,34 +504,31 @@ function ChatPage() {
</div> </div>
</button> </button>
))} ))}
{conversations.some(c => c.status === "CLOSED") && (
<>
<button
style={s.closedSectionToggle}
onClick={() => setClosedExpanded(p => !p)}
>
<span>Closed ({conversations.filter(c => c.status === "CLOSED").length})</span>
<span>{closedExpanded ? "▲" : "▼"}</span>
</button>
{closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => (
<button
key={conv.id}
style={{ ...s.convItem, ...s.convItemClosed, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
onClick={() => switchConversation(conv.id)}
>
<div style={s.convItemTop}>
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
<span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span>
</div>
<div style={s.convItemBottom}>
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
</div>
</button>
))}
</>
)}
</div> </div>
{conversations.some(c => c.status === "CLOSED") && (
<>
<button style={s.closedSectionToggle} onClick={() => setClosedExpanded(p => !p)}>
<span>Closed ({conversations.filter(c => c.status === "CLOSED").length})</span>
<span>{closedExpanded ? "▲" : "▼"}</span>
</button>
{closedExpanded && conversations.filter(c => c.status === "CLOSED").map((conv) => (
<button
key={conv.id}
style={{ ...s.convItem, ...s.convItemClosed, ...(conv.id === conversation?.id ? s.convItemActive : {}) }}
onClick={() => switchConversation(conv.id)}
>
<div style={s.convItemTop}>
<span style={s.convItemSubject}>{conv.subject || `Conversation #${conv.id}`}</span>
<span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span>
</div>
<div style={s.convItemBottom}>
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span>
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
</div>
</button>
))}
</>
)}
<button style={s.newConvSidebarBtn} onClick={() => handleNewConversation()}> <button style={s.newConvSidebarBtn} onClick={() => handleNewConversation()}>
+ New Conversation + New Conversation
</button> </button>

View File

@@ -50,7 +50,7 @@ export default function ContactPage() {
setSendSuccess(true); setSendSuccess(true);
setSubject(""); setSubject("");
setBody(""); setBody("");
} catch (err) { } catch {
setSendError("Failed to send message. Please try again."); setSendError("Failed to send message. Please try again.");
} finally { } finally {
setSending(false); setSending(false);
@@ -61,67 +61,63 @@ export default function ContactPage() {
<main className="info-page"> <main className="info-page">
<section className="info-hero"> <section className="info-hero">
<h1 className="info-title">Contact Us</h1> <h1 className="info-title">Contact Us</h1>
<p className="info-subtitle">Reach the team, find a location, or connect with store personnel.</p> <p className="info-subtitle">Reach the team, find a location, or send us a message.</p>
<div className="title-decoration"></div> <div className="title-decoration"></div>
</section> </section>
<section className="info-content"> <section className="contact-layout">
<div className="info-card"> <div className="info-card">
<h2>General Contact</h2> <h2>Get in Touch</h2>
<p>Email: hello@leonspetstore.com.au</p> <p>Email: hello@leonspetstore.com.au</p>
<p>Phone: (03) 9000 0000</p> <p>Phone: (03) 9000 0000</p>
<p>Hours: MonSat, 9:00 AM 6:00 PM</p> <p>Hours: MonSat, 9:00 AM 6:00 PM</p>
</div>
{token && ( {token && (
<div className="info-card"> <div className="contact-form-section">
<h2>Send Us a Message</h2> <h3>Send Us a Message</h3>
{sendSuccess ? ( {sendSuccess ? (
<p className="contact-success">Your message has been sent. We&apos;ll be in touch soon.</p> <p className="contact-success">Your message has been sent. We&apos;ll be in touch soon.</p>
) : ( ) : (
<form className="auth-form" onSubmit={handleSend}> <form className="auth-form" onSubmit={handleSend}>
<label className="auth-label"> <label className="auth-label">
Subject Subject
<input <input
className="auth-input" className="auth-input"
type="text" type="text"
value={subject} value={subject}
onChange={(e) => setSubject(e.target.value)} onChange={(e) => setSubject(e.target.value)}
required required
maxLength={150} maxLength={150}
/> />
</label> </label>
<label className="auth-label"> <label className="auth-label">
Message Message
<textarea <textarea
className="auth-input" className="auth-input"
style={{ resize: "vertical" }} style={{ resize: "vertical" }}
value={body} value={body}
onChange={(e) => setBody(e.target.value)} onChange={(e) => setBody(e.target.value)}
required required
maxLength={2000} maxLength={2000}
rows={6} rows={5}
/> />
</label> </label>
{sendError && <p className="contact-error">{sendError}</p>} {sendError && <p className="contact-error">{sendError}</p>}
<button className="auth-submit-btn" type="submit" disabled={sending}> <button className="auth-submit-btn" type="submit" disabled={sending}>
{sending ? "Sending…" : "Send Message"} {sending ? "Sending…" : "Send Message"}
</button> </button>
</form> </form>
)} )}
</div> </div>
)} )}
</div>
<div className="info-card"> <div className="info-card">
<h2>Store Locations</h2> <h2>Store Locations</h2>
{loading && <p>Loading locations...</p>} {loading && <p>Loading locations...</p>}
{error && <p style={{ color: "red" }}>Failed to load locations: {error}</p>} {error && <p style={{ color: "red" }}>Failed to load locations: {error}</p>}
{!loading && !error && locations.length === 0 && <p>No store locations found.</p>}
{!loading && !error && locations.length === 0 && (
<p>No store locations found.</p>
)}
{!loading && !error && locations.length > 0 && ( {!loading && !error && locations.length > 0 && (
<div className="info-card-grid"> <div className="info-card-grid">

View File

@@ -3132,6 +3132,10 @@ img, video, iframe {
max-width: 100%; max-width: 100%;
} }
.contact-layout { display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; max-width: 1200px; margin: 0 auto; padding: 0 2rem 3rem; }
@media (max-width: 768px) { .contact-layout { grid-template-columns: 1fr; } }
.contact-form-section { margin-top: 1.5rem; border-top: 1px solid #f0f0f0; padding-top: 1.5rem; }
.contact-form-section h3 { margin: 0 0 1rem; font-size: 1rem; color: #333; }
.contact-form { display: flex; flex-direction: column; gap: 1rem; } .contact-form { display: flex; flex-direction: column; gap: 1rem; }
.contact-label { display: flex; flex-direction: column; gap: 0.4rem; font-weight: 500; color: #333; font-size: 0.95rem; } .contact-label { display: flex; flex-direction: column; gap: 0.4rem; font-weight: 500; color: #333; font-size: 0.95rem; }
.contact-input, .contact-textarea { border: 1px solid #ddd; border-radius: 8px; padding: 0.6rem 0.8rem; font-size: 0.95rem; font-family: inherit; resize: vertical; } .contact-input, .contact-textarea { border: 1px solid #ddd; border-radius: 8px; padding: 0.6rem 0.8rem; font-size: 0.95rem; font-family: inherit; resize: vertical; }