fix contact layout and chat ui
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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: Mon–Sat, 9:00 AM – 6:00 PM</p>
|
<p>Hours: Mon–Sat, 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'll be in touch soon.</p>
|
<p className="contact-success">Your message has been sent. We'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">
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user