Merge pull request #335 from RecentRunner/web-lastfixes

web last fixes
This commit is contained in:
2026-04-20 11:23:59 -06:00
committed by GitHub
19 changed files with 102 additions and 44 deletions

View File

@@ -131,7 +131,7 @@ export default function AdoptPage() {
<input <input
className={`${inputCls} flex-1 max-w-[400px] font-[inherit] max-[600px]:max-w-full`} className={`${inputCls} flex-1 max-w-[400px] font-[inherit] max-[600px]:max-w-full`}
type="text" type="text"
placeholder="Search by name, species, or breed..." placeholder="Search"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />

View File

@@ -68,11 +68,23 @@ function AttachmentPreview({ url, name, token }) {
); );
} }
function useIsMobile() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const check = () => setIsMobile(window.innerWidth < 640);
check();
window.addEventListener("resize", check);
return () => window.removeEventListener("resize", check);
}, []);
return isMobile;
}
function AiChatPage() { function AiChatPage() {
const { user, token, loading: authLoading } = useAuth(); const { user, token, loading: authLoading } = useAuth();
const router = useRouter(); const router = useRouter();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const conversationIdParam = searchParams.get("id"); const conversationIdParam = searchParams.get("id");
const isMobile = useIsMobile();
const [conversation, setConversation] = useState(null); const [conversation, setConversation] = useState(null);
const [messages, setMessages] = useState([]); const [messages, setMessages] = useState([]);
@@ -498,8 +510,8 @@ function AiChatPage() {
</section> </section>
<section style={s.chatSection}> <section style={s.chatSection}>
<div style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}> <div style={{ display: "flex", flexDirection: isMobile ? "column" : "row", gap: "1rem", alignItems: "flex-start" }}>
<div style={s.sidebar}> <div style={{ ...s.sidebar, width: isMobile ? "100%" : 230, maxHeight: isMobile ? 260 : "calc(100vh - 220px)" }}>
<div style={s.sidebarHeader}> <div style={s.sidebarHeader}>
<span style={s.sidebarTitle}>All Conversations</span> <span style={s.sidebarTitle}>All Conversations</span>
</div> </div>
@@ -519,7 +531,10 @@ function AiChatPage() {
<span style={{ ...s.convStatusBadge, ...s.convStatusOpen }}>{conv.status}</span> <span style={{ ...s.convStatusBadge, ...s.convStatusOpen }}>{conv.status}</span>
</div> </div>
<div style={s.convItemBottom}> <div style={s.convItemBottom}>
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span> <span style={{ ...s.convItemMode, display: "flex", alignItems: "center", gap: "0.25rem" }}>
<img src={conv.mode === "HUMAN" ? "/bootstrap/person-fill.svg" : "/bootstrap/robot.svg"} alt="" style={{ width: 12, height: 12 }} />
{conv.mode === "HUMAN" ? "Live" : "AI"}
</span>
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span> <span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
</div> </div>
</button> </button>
@@ -542,7 +557,10 @@ function AiChatPage() {
<span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span> <span style={{ ...s.convStatusBadge, ...s.convStatusClosed }}>CLOSED</span>
</div> </div>
<div style={s.convItemBottom}> <div style={s.convItemBottom}>
<span style={s.convItemMode}>{conv.mode === "HUMAN" ? "👤 Live" : "🤖 AI"}</span> <span style={{ ...s.convItemMode, display: "flex", alignItems: "center", gap: "0.25rem" }}>
<img src={conv.mode === "HUMAN" ? "/bootstrap/person-fill.svg" : "/bootstrap/robot.svg"} alt="" style={{ width: 12, height: 12 }} />
{conv.mode === "HUMAN" ? "Live" : "AI"}
</span>
<span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span> <span style={s.convItemDate}>{conv.createdAt ? new Date(conv.createdAt).toLocaleDateString() : ""}</span>
</div> </div>
</button> </button>
@@ -554,10 +572,10 @@ function AiChatPage() {
</button> </button>
</div> </div>
<div style={{ flex: 1, minWidth: 0 }}> <div style={{ flex: 1, minWidth: 0, width: isMobile ? "100%" : undefined }}>
{!conversation ? ( {!conversation ? (
<div style={s.noConvCard}> <div style={s.noConvCard}>
<div style={s.noConvIcon}>🐾</div> <div style={s.noConvIcon}><img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "3rem", height: "3rem", opacity: 0.6 }} /></div>
<h2 style={s.noConvTitle}>No active conversation</h2> <h2 style={s.noConvTitle}>No active conversation</h2>
<p style={s.noConvText}>Start a new conversation with the AI assistant.</p> <p style={s.noConvText}>Start a new conversation with the AI assistant.</p>
{error && <div style={s.errorInline}>{error}</div>} {error && <div style={s.errorInline}>{error}</div>}
@@ -570,9 +588,9 @@ function AiChatPage() {
</div> </div>
) : ( ) : (
<div style={s.chatCard}> <div style={s.chatCard}>
<div style={s.chatHeader}> <div style={{ ...s.chatHeader, flexDirection: isMobile ? "column" : "row", alignItems: isMobile ? "flex-start" : "center", gap: isMobile ? "0.6rem" : 0 }}>
<div style={s.chatHeaderLeft}> <div style={s.chatHeaderLeft}>
<div style={isEscalated ? s.agentAvatar : s.aiAvatar}>{isEscalated ? "👤" : "🐾"}</div> <div style={isEscalated ? s.agentAvatar : s.aiAvatar}>{isEscalated ? "👤" : <img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "100%", height: "100%", filter: "brightness(0) invert(1)" }} />}</div>
<div> <div>
<div style={s.chatHeaderTitle}> <div style={s.chatHeaderTitle}>
{isEscalated ? (hasStaff ? "Support Agent" : "Leon's Pet Store Support") : "Leon's Pet Assistant"} {isEscalated ? (hasStaff ? "Support Agent" : "Leon's Pet Store Support") : "Leon's Pet Assistant"}
@@ -583,14 +601,14 @@ function AiChatPage() {
</div> </div>
</div> </div>
</div> </div>
<div style={{ display: "flex", gap: "0.5rem" }}> <div style={{ display: "flex", gap: "0.5rem", width: isMobile ? "100%" : undefined }}>
{!isEscalated && !isClosed && ( {!isEscalated && !isClosed && (
<button style={s.humanBtn} onClick={handleSwitchToHuman} title="Connect with a human support agent"> <button style={isMobile ? { ...s.humanBtn, flex: 1 } : s.humanBtn} onClick={handleSwitchToHuman} title="Connect with a human support agent">
Chat with a Real Person Chat with a Real Person
</button> </button>
)} )}
{!isClosed && ( {!isClosed && (
<button style={s.closeConvBtn} onClick={handleCloseConversation} title="Close this conversation"> <button style={isMobile ? { ...s.closeConvBtn, flex: 1 } : s.closeConvBtn} onClick={handleCloseConversation} title="Close this conversation">
Close Chat Close Chat
</button> </button>
)} )}
@@ -607,7 +625,7 @@ function AiChatPage() {
<div style={s.messagesArea} ref={messagesAreaRef}> <div style={s.messagesArea} ref={messagesAreaRef}>
{messages.length === 0 && ( {messages.length === 0 && (
<div style={s.emptyState}> <div style={s.emptyState}>
<div style={s.emptyIcon}>{isEscalated ? "💬" : "🐾"}</div> <div style={s.emptyIcon}>{isEscalated ? "💬" : <img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "3rem", height: "3rem", opacity: 0.6 }} />}</div>
<p style={s.emptyText}> <p style={s.emptyText}>
{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!`} {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!`}
</p> </p>
@@ -624,7 +642,7 @@ function AiChatPage() {
...(isOwn ? s.messageRowUser : s.messageRowAgent), ...(isOwn ? s.messageRowUser : s.messageRowAgent),
}} }}
> >
{!isOwn && <div style={isEscalated ? s.agentAvatarSmall : s.aiAvatarSmall}>{isEscalated ? "👤" : "🐾"}</div>} {!isOwn && <div style={isEscalated ? s.agentAvatarSmall : s.aiAvatarSmall}>{isEscalated ? "👤" : <img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "100%", height: "100%", filter: "brightness(0) invert(1)" }} />}</div>}
<div <div
style={{ style={{
...s.messageBubble, ...s.messageBubble,
@@ -655,7 +673,7 @@ function AiChatPage() {
{botTyping && !isEscalated && ( {botTyping && !isEscalated && (
<div style={{ ...s.messageRow, ...s.messageRowAgent }}> <div style={{ ...s.messageRow, ...s.messageRowAgent }}>
<div style={s.aiAvatarSmall}>🐾</div> <div style={s.aiAvatarSmall}><img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "100%", height: "100%", filter: "brightness(0) invert(1)" }} /></div>
<div style={{ ...s.messageBubble, ...s.bubbleAgent, display: "flex", alignItems: "center", gap: "4px", padding: "0.6rem 0.9rem" }}> <div style={{ ...s.messageBubble, ...s.bubbleAgent, display: "flex", alignItems: "center", gap: "4px", padding: "0.6rem 0.9rem" }}>
<span className="fc-dot" /> <span className="fc-dot" />
<span className="fc-dot" style={{ animationDelay: "0.2s" }} /> <span className="fc-dot" style={{ animationDelay: "0.2s" }} />
@@ -792,7 +810,6 @@ const s = {
padding: "1.5rem 1rem 2rem", padding: "1.5rem 1rem 2rem",
}, },
sidebar: { sidebar: {
width: 230,
flexShrink: 0, flexShrink: 0,
background: "white", background: "white",
borderRadius: 16, borderRadius: 16,
@@ -800,8 +817,7 @@ const s = {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
overflow: "hidden", overflow: "hidden",
maxHeight: "calc(100vh - 220px)", minHeight: 200,
minHeight: 300,
}, },
sidebarHeader: { sidebarHeader: {
display: "flex", display: "flex",
@@ -901,7 +917,7 @@ const s = {
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
height: "calc(100vh - 220px)", height: "calc(100vh - 220px)",
minHeight: 450, minHeight: 400,
}, },
chatHeader: { chatHeader: {
display: "flex", display: "flex",
@@ -922,6 +938,7 @@ const s = {
height: 44, height: 44,
borderRadius: "50%", borderRadius: "50%",
background: "linear-gradient(135deg, #444, #666)", background: "linear-gradient(135deg, #444, #666)",
border: "3px solid #666",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
@@ -953,8 +970,8 @@ const s = {
border: "2px solid #ff8c00", border: "2px solid #ff8c00",
color: "#ff8c00", color: "#ff8c00",
borderRadius: 8, borderRadius: 8,
padding: "0.45rem 0.9rem", padding: "0.3rem 0.65rem",
fontSize: "0.82rem", fontSize: "0.72rem",
fontWeight: 600, fontWeight: 600,
cursor: "pointer", cursor: "pointer",
whiteSpace: "nowrap", whiteSpace: "nowrap",
@@ -997,8 +1014,8 @@ const s = {
border: "2px solid #c0392b", border: "2px solid #c0392b",
color: "#c0392b", color: "#c0392b",
borderRadius: 8, borderRadius: 8,
padding: "0.45rem 0.9rem", padding: "0.3rem 0.65rem",
fontSize: "0.82rem", fontSize: "0.72rem",
fontWeight: 600, fontWeight: 600,
cursor: "pointer", cursor: "pointer",
whiteSpace: "nowrap", whiteSpace: "nowrap",
@@ -1105,6 +1122,7 @@ const s = {
height: 30, height: 30,
borderRadius: "50%", borderRadius: "50%",
background: "linear-gradient(135deg, #444, #666)", background: "linear-gradient(135deg, #444, #666)",
border: "3px solid #666",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",

View File

@@ -66,11 +66,11 @@ export default function Home() {
</div> </div>
<div className="max-w-[1200px] mx-auto px-8 pb-6 grid grid-cols-3 gap-6 max-[768px]:grid-cols-1"> <div className="max-w-[1200px] mx-auto px-8 pb-6 grid grid-cols-3 gap-6 max-[768px]:grid-cols-1">
<div className="bg-white rounded-2xl shadow-[0_4px_12px_rgba(0,0,0,0.08)] p-6"> <div className="bg-white rounded-2xl shadow-[0_4px_12px_rgba(0,0,0,0.08)] p-6">
<h3 className="mt-0 mb-4 text-[#222]">What We Do</h3> <h3 className="mt-0 mb-4 text-[#222] underline decoration-[#e68672] underline-offset-4 text-[1.3rem] font-bold">What We Do</h3>
<p>Leon&apos;s Pet Store is a full-service pet shop offering adoptions, grooming, veterinary appointments, and a wide range of supplies to keep your pets happy and healthy.</p> <p>Leon&apos;s Pet Store is a full-service pet shop offering adoptions, grooming, veterinary appointments, and a wide range of supplies to keep your pets happy and healthy.</p>
</div> </div>
<div className="bg-white rounded-2xl shadow-[0_4px_12px_rgba(0,0,0,0.08)] p-6"> <div className="bg-white rounded-2xl shadow-[0_4px_12px_rgba(0,0,0,0.08)] p-6">
<h3 className="mt-0 mb-4 text-[#222]">Our Focus</h3> <h3 className="mt-0 mb-4 text-[#222] underline decoration-[#e68672] underline-offset-4 text-[1.3rem] font-bold">Our Focus</h3>
<ul className="m-0 pl-5 grid gap-2 list-disc"> <ul className="m-0 pl-5 grid gap-2 list-disc">
<li>Support responsible pet adoption</li> <li>Support responsible pet adoption</li>
<li>Provide grooming and care services</li> <li>Provide grooming and care services</li>
@@ -79,7 +79,7 @@ export default function Home() {
</ul> </ul>
</div> </div>
<div className="bg-white rounded-2xl shadow-[0_4px_12px_rgba(0,0,0,0.08)] p-6"> <div className="bg-white rounded-2xl shadow-[0_4px_12px_rgba(0,0,0,0.08)] p-6">
<h3 className="mt-0 mb-4 text-[#222]">Visit the Store</h3> <h3 className="mt-0 mb-4 text-[#222] underline decoration-[#e68672] underline-offset-4 text-[1.3rem] font-bold">Visit the Store</h3>
<p>Come visit us in person or explore our services online. Whether you&apos;re a first-time pet owner or a seasoned animal lover, we&apos;re here to help every step of the way.</p> <p>Come visit us in person or explore our services online. Whether you&apos;re a first-time pet owner or a seasoned animal lover, we&apos;re here to help every step of the way.</p>
</div> </div>
</div> </div>

View File

@@ -56,7 +56,7 @@ export default function ProductsPage() {
<input <input
className="flex-1 max-w-[400px] px-4 py-[0.6rem] border-2 border-[#ddd] rounded-md text-base outline-none transition-colors focus:border-[#e68672] font-[inherit]" className="flex-1 max-w-[400px] px-4 py-[0.6rem] border-2 border-[#ddd] rounded-md text-base outline-none transition-colors focus:border-[#e68672] font-[inherit]"
type="text" type="text"
placeholder="Search by name or category..." placeholder="Search"
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />

View File

@@ -444,7 +444,7 @@ export default function ProfilePage() {
{fields.map(({ label, value }) => ( {fields.map(({ label, value }) => (
<div key={label} className="flex gap-2 py-1"> <div key={label} className="flex gap-2 py-1">
<dt className="text-[0.85rem] font-semibold text-[#888] min-w-[100px]">{label}</dt> <dt className="text-[0.85rem] font-semibold text-[#888] min-w-[100px]">{label}</dt>
<dd className="text-[0.9rem] text-[#333] m-0">{value}</dd> <dd className="text-[0.9rem] text-[#333] m-0 break-words min-w-0">{value}</dd>
</div> </div>
))} ))}
</dl> </dl>
@@ -552,7 +552,7 @@ export default function ProfilePage() {
</select> </select>
</label> </label>
<div className="flex gap-3"> <div className="flex gap-3">
<button type="submit" className={submitBtnCls} disabled={submitting}> <button type="submit" className={`${submitBtnCls} px-5 py-2 text-[0.9rem]`} disabled={submitting}>
{submitting ? "Saving..." : editingPet ? "Save Changes" : "Add Pet"} {submitting ? "Saving..." : editingPet ? "Save Changes" : "Add Pet"}
</button> </button>
<button type="button" className="px-4 py-2 border border-[#ddd] rounded-lg bg-white text-[#555] text-[0.9rem] cursor-pointer hover:border-[#aaa] transition-colors" onClick={closeForm}> <button type="button" className="px-4 py-2 border border-[#ddd] rounded-lg bg-white text-[#555] text-[0.9rem] cursor-pointer hover:border-[#aaa] transition-colors" onClick={closeForm}>

View File

@@ -20,6 +20,7 @@ export default function FloatingChat() {
} = useChatWidget(); } = useChatWidget();
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const [fabHovered, setFabHovered] = useState(false);
const messagesEndRef = useRef(null); const messagesEndRef = useRef(null);
const prevAiLengthRef = useRef(0); const prevAiLengthRef = useRef(0);
const prevLiveLengthRef = useRef(0); const prevLiveLengthRef = useRef(0);
@@ -65,8 +66,15 @@ const prevLiveLengthRef = useRef(0);
return ( return (
<> <>
{/* Floating toggle button */} {/* Floating toggle button */}
<button onClick={toggleOpen} style={s.fab} aria-label={isOpen ? "Close chat" : "Open chat"}> <button onClick={toggleOpen} style={s.fab} aria-label={isOpen ? "Close chat" : "Open chat"} onMouseEnter={() => setFabHovered(true)} onMouseLeave={() => setFabHovered(false)}>
<span style={{ fontSize: "1.4rem", lineHeight: 1 }}>{isOpen ? "✕" : "💬"}</span> {isOpen ? (
<span style={{ fontSize: "1.4rem", lineHeight: 1 }}></span>
) : (
<span style={{ position: "relative", width: 24, height: 24, display: "inline-block" }}>
<img src="/bootstrap/chat.svg" alt="" style={{ width: 24, height: 24, position: "absolute", inset: 0, filter: "brightness(0) invert(1)", opacity: fabHovered ? 0 : 1, transition: "opacity 0.15s" }} />
<img src="/bootstrap/chat-fill.svg" alt="" style={{ width: 24, height: 24, position: "absolute", inset: 0, filter: "brightness(0) invert(1)", opacity: fabHovered ? 1 : 0, transition: "opacity 0.15s" }} />
</span>
)}
{!isOpen && openConvCount > 0 && <span style={s.fabBadge}>{openConvCount}</span>} {!isOpen && openConvCount > 0 && <span style={s.fabBadge}>{openConvCount}</span>}
</button> </button>
@@ -77,7 +85,7 @@ const prevLiveLengthRef = useRef(0);
{/* Header */} {/* Header */}
<div style={s.header}> <div style={s.header}>
<div style={s.headerLeft}> <div style={s.headerLeft}>
<div style={s.headerAvatar}>🐾</div> <div style={s.headerAvatar}><img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "100%", height: "100%", filter: "brightness(0) invert(1)" }} /></div>
<div> <div>
<div style={s.headerTitle}>Leon's Assistant</div> <div style={s.headerTitle}>Leon's Assistant</div>
<div style={s.headerSub}> <div style={s.headerSub}>
@@ -99,7 +107,7 @@ const prevLiveLengthRef = useRef(0);
{/* Guest */} {/* Guest */}
{!user && ( {!user && (
<div style={s.guestBody}> <div style={s.guestBody}>
<span style={{ fontSize: "2.5rem" }}>🐾</span> <img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "2.5rem", height: "2.5rem", opacity: 0.6 }} />
<p style={{ color: "#555", fontSize: "0.95rem", margin: "0.75rem 0 1.25rem", textAlign: "center" }}> <p style={{ color: "#555", fontSize: "0.95rem", margin: "0.75rem 0 1.25rem", textAlign: "center" }}>
Log in to chat with our pet assistant! Log in to chat with our pet assistant!
</p> </p>
@@ -129,7 +137,7 @@ const prevLiveLengthRef = useRef(0);
{!convsLoading && conversations.length === 0 && ( {!convsLoading && conversations.length === 0 && (
<div style={{ ...s.empty, flex: 1 }}> <div style={{ ...s.empty, flex: 1 }}>
<span style={{ fontSize: "2rem" }}>💬</span> <img src="/bootstrap/chat.svg" alt="chat" style={{ width: 32, height: 32, opacity: 0.5 }} />
<p style={s.emptyText}>No conversations yet.<br />Start a live chat above.</p> <p style={s.emptyText}>No conversations yet.<br />Start a live chat above.</p>
</div> </div>
)} )}
@@ -223,7 +231,7 @@ const prevLiveLengthRef = useRef(0);
return ( return (
<div key={msg.id} style={{ ...s.row, ...(isUser ? s.rowUser : s.rowOther) }}> <div key={msg.id} style={{ ...s.row, ...(isUser ? s.rowUser : s.rowOther) }}>
{!isUser && ( {!isUser && (
<div style={s.otherAvatar}>{msg.senderRole === "BOT" ? "🐾" : "👤"}</div> <div style={s.otherAvatar}><img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "100%", height: "100%" }} /></div>
)} )}
<div style={{ ...s.bubble, ...(isUser ? s.bubbleUser : s.bubbleOther) }}> <div style={{ ...s.bubble, ...(isUser ? s.bubbleUser : s.bubbleOther) }}>
{!isUser && ( {!isUser && (
@@ -282,7 +290,7 @@ const prevLiveLengthRef = useRef(0);
<div style={s.messages}> <div style={s.messages}>
{aiMessages.length === 0 && ( {aiMessages.length === 0 && (
<div style={s.empty}> <div style={s.empty}>
<span style={{ fontSize: "2rem" }}>🐾</span> <img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "2rem", height: "2rem", opacity: 0.5 }} />
<p style={s.emptyText}> <p style={s.emptyText}>
Hi{user?.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}!<br /> Hi{user?.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}!<br />
Ask me anything about pets. Ask me anything about pets.
@@ -292,7 +300,7 @@ const prevLiveLengthRef = useRef(0);
{aiMessages.map((msg) => ( {aiMessages.map((msg) => (
<div key={msg.id} style={{ ...s.row, ...(msg.role === "user" ? s.rowUser : s.rowOther) }}> <div key={msg.id} style={{ ...s.row, ...(msg.role === "user" ? s.rowUser : s.rowOther) }}>
{msg.role === "assistant" && <div style={s.otherAvatar}>🐾</div>} {msg.role === "assistant" && <div style={s.otherAvatar}><img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "100%", height: "100%" }} /></div>}
<div style={{ ...s.bubble, ...(msg.role === "user" ? s.bubbleUser : s.bubbleOther) }}> <div style={{ ...s.bubble, ...(msg.role === "user" ? s.bubbleUser : s.bubbleOther) }}>
{msg.content.split("\n").map((line, i, arr) => ( {msg.content.split("\n").map((line, i, arr) => (
<span key={i}>{line}{i < arr.length - 1 && <br />}</span> <span key={i}>{line}{i < arr.length - 1 && <br />}</span>
@@ -308,7 +316,7 @@ const prevLiveLengthRef = useRef(0);
{aiSending && ( {aiSending && (
<div style={{ ...s.row, ...s.rowOther }}> <div style={{ ...s.row, ...s.rowOther }}>
<div style={s.otherAvatar}>🐾</div> <div style={s.otherAvatar}><img src="/bootstrap/person-circle.svg" alt="assistant" style={{ width: "100%", height: "100%" }} /></div>
<div style={{ ...s.bubble, ...s.bubbleOther, ...s.typingBubble }}> <div style={{ ...s.bubble, ...s.bubbleOther, ...s.typingBubble }}>
<span className="fc-dot" /> <span className="fc-dot" />
<span className="fc-dot" style={{ animationDelay: "0.2s" }} /> <span className="fc-dot" style={{ animationDelay: "0.2s" }} />

View File

@@ -14,8 +14,11 @@ const cartBadgeCls = "absolute -top-1 -right-1.5 bg-[#e53935] text-white rounded
function CartIcon({ itemCount, onClick }) { function CartIcon({ itemCount, onClick }) {
return ( return (
<Link href="/cart" className={cartBtnCls} aria-label="Cart" onClick={onClick}> <Link href="/cart" className={`${cartBtnCls} group`} aria-label="Cart" onClick={onClick}>
🛒 <span className="relative w-6 h-6 inline-block">
<img src="/bootstrap/cart.svg" alt="" className="w-6 h-6 absolute inset-0 transition-opacity duration-150 group-hover:opacity-0" />
<img src="/bootstrap/cart-fill.svg" alt="" className="w-6 h-6 absolute inset-0 transition-opacity duration-150 opacity-0 group-hover:opacity-100" />
</span>
{itemCount > 0 && ( {itemCount > 0 && (
<span className={cartBadgeCls}>{itemCount > 99 ? "99+" : itemCount}</span> <span className={cartBadgeCls}>{itemCount > 99 ? "99+" : itemCount}</span>
)} )}
@@ -59,7 +62,7 @@ export default function DisplayNav() {
return ( return (
<nav className="fixed top-0 left-0 w-full bg-[#e68672] shadow-[0_2px_10px_rgba(0,0,0,0.1)] z-[1000] px-8 py-2 grid [grid-template-columns:1fr_auto_1fr] items-center min-h-[70px] max-[1100px]:px-4"> <nav className="fixed top-0 left-0 w-full bg-[#e68672] shadow-[0_2px_10px_rgba(0,0,0,0.1)] z-[1000] px-8 py-2 grid [grid-template-columns:1fr_auto_1fr] items-center min-h-[70px] max-[1100px]:px-4">
<Link href="/" onClick={closeMenu}> <Link href="/" onClick={closeMenu}>
<Image className="mx-3" src="/logo_simple.png" alt="store_logo" width={50} height={50} id="logo" loading="eager" /> <Image className="mx-3" src="/logo.png" alt="store_logo" width={50} height={50} id="logo" loading="eager" />
</Link> </Link>
{/* Desktop nav links */} {/* Desktop nav links */}

View File

@@ -26,6 +26,7 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes
await addItem(prodId, quantity); await addItem(prodId, quantity);
setFeedback({ type: "success", message: `${quantity} × ${prodName} added to cart!` }); setFeedback({ type: "success", message: `${quantity} × ${prodName} added to cart!` });
setQuantity(1); setQuantity(1);
setTimeout(() => setFeedback(null), 1000);
} catch (err) { } catch (err) {
setFeedback({ type: "error", message: err.message ?? "Failed to add to cart." }); setFeedback({ type: "error", message: err.message ?? "Failed to add to cart." });
} finally { } finally {
@@ -90,15 +91,17 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes
</div> </div>
<button <button
className="w-full py-[0.85rem] bg-[#e68672] text-[#2f2f2f] border-none rounded-[10px] text-base font-bold cursor-pointer transition-all hover:bg-[#d4705e] hover:text-white active:scale-[0.98] disabled:opacity-60 disabled:cursor-not-allowed" className="w-full py-[0.85rem] border-none rounded-[10px] text-base font-bold cursor-pointer transition-all active:scale-[0.98] disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center gap-2"
style={feedback?.type === "success" ? { background: "#2ecc71", color: "white", boxShadow: "0 0 16px 4px rgba(46,204,113,0.6)" } : { background: "#e68672", color: "#2f2f2f" }}
onClick={handleAddToCart} onClick={handleAddToCart}
disabled={adding} disabled={adding}
> >
<img src="/bootstrap/cart-plus-fill.svg" alt="" className="w-5 h-5" style={{ filter: "brightness(0) invert(0.2)" }} />
{adding ? "Adding…" : "Add to Cart"} {adding ? "Adding…" : "Add to Cart"}
</button> </button>
{feedback && ( {feedback?.type === "error" && (
<p className={`mt-3 text-[0.9rem] rounded-lg px-4 py-[0.6rem] ${feedback.type === "success" ? "bg-[#f0fff4] border border-[#b2dfdb] text-[#1a7a3c]" : "bg-[#fff0f0] border border-[#f5c6c6] text-[#c0392b]"}`}> <p className="mt-3 text-[0.9rem] rounded-lg px-4 py-[0.6rem] m-0 bg-[#fff0f0] border border-[#f5c6c6] text-[#c0392b]">
{feedback.message} {feedback.message}
</p> </p>
)} )}

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cart-fill" viewBox="0 0 16 16">
<path d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .491.592l-1.5 8A.5.5 0 0 1 13 12H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5M5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4m7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4m-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2m7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
</svg>

After

Width:  |  Height:  |  Size: 440 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cart-plus-fill" viewBox="0 0 16 16">
<path d="M.5 1a.5.5 0 0 0 0 1h1.11l.401 1.607 1.498 7.985A.5.5 0 0 0 4 12h1a2 2 0 1 0 0 4 2 2 0 0 0 0-4h7a2 2 0 1 0 0 4 2 2 0 0 0 0-4h1a.5.5 0 0 0 .491-.408l1.5-8A.5.5 0 0 0 14.5 3H2.89l-.405-1.621A.5.5 0 0 0 2 1zM6 14a1 1 0 1 1-2 0 1 1 0 0 1 2 0m7 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0M9 5.5V7h1.5a.5.5 0 0 1 0 1H9v1.5a.5.5 0 0 1-1 0V8H6.5a.5.5 0 0 1 0-1H8V5.5a.5.5 0 0 1 1 0"/>
</svg>

After

Width:  |  Height:  |  Size: 513 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cart" viewBox="0 0 16 16">
<path d="M0 1.5A.5.5 0 0 1 .5 1H2a.5.5 0 0 1 .485.379L2.89 3H14.5a.5.5 0 0 1 .491.592l-1.5 8A.5.5 0 0 1 13 12H4a.5.5 0 0 1-.491-.408L2.01 3.607 1.61 2H.5a.5.5 0 0 1-.5-.5M3.102 4l1.313 7h8.17l1.313-7zM5 12a2 2 0 1 0 0 4 2 2 0 0 0 0-4m7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4m-7 1a1 1 0 1 1 0 2 1 1 0 0 1 0-2m7 0a1 1 0 1 1 0 2 1 1 0 0 1 0-2"/>
</svg>

After

Width:  |  Height:  |  Size: 465 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat-fill" viewBox="0 0 16 16">
<path d="M8 15c4.418 0 8-3.134 8-7s-3.582-7-8-7-8 3.134-8 7c0 1.76.743 3.37 1.97 4.6-.097 1.016-.417 2.13-.771 2.966-.079.186.074.394.273.362 2.256-.37 3.597-.938 4.18-1.234A9 9 0 0 0 8 15"/>
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-chat" viewBox="0 0 16 16">
<path d="M2.678 11.894a1 1 0 0 1 .287.801 11 11 0 0 1-.398 2c1.395-.323 2.247-.697 2.634-.893a1 1 0 0 1 .71-.074A8 8 0 0 0 8 14c3.996 0 7-2.807 7-6s-3.004-6-7-6-7 2.808-7 6c0 1.468.617 2.83 1.678 3.894m-.493 3.905a22 22 0 0 1-.713.129c-.2.032-.352-.176-.273-.362a10 10 0 0 0 .244-.637l.003-.01c.248-.72.45-1.548.524-2.319C.743 11.37 0 9.76 0 8c0-3.866 3.582-7 8-7s8 3.134 8 7-3.582 7-8 7a9 9 0 0 1-2.347-.306c-.52.263-1.639.742-3.468 1.105"/>
</svg>

After

Width:  |  Height:  |  Size: 574 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-circle" viewBox="0 0 16 16">
<path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/>
<path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"/>
</svg>

After

Width:  |  Height:  |  Size: 344 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-person-fill" viewBox="0 0 16 16">
<path d="M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6"/>
</svg>

After

Width:  |  Height:  |  Size: 222 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-robot" viewBox="0 0 16 16">
<path d="M6 12.5a.5.5 0 0 1 .5-.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1-.5-.5M3 8.062C3 6.76 4.235 5.765 5.53 5.886a26.6 26.6 0 0 0 4.94 0C11.765 5.765 13 6.76 13 8.062v1.157a.93.93 0 0 1-.765.935c-.845.147-2.34.346-4.235.346s-3.39-.2-4.235-.346A.93.93 0 0 1 3 9.219zm4.542-.827a.25.25 0 0 0-.217.068l-.92.9a25 25 0 0 1-1.871-.183.25.25 0 0 0-.068.495c.55.076 1.232.149 2.02.193a.25.25 0 0 0 .189-.071l.754-.736.847 1.71a.25.25 0 0 0 .404.062l.932-.97a25 25 0 0 0 1.922-.188.25.25 0 0 0-.068-.495c-.538.074-1.207.145-1.98.189a.25.25 0 0 0-.166.076l-.754.785-.842-1.7a.25.25 0 0 0-.182-.135"/>
<path d="M8.5 1.866a1 1 0 1 0-1 0V3h-2A4.5 4.5 0 0 0 1 7.5V8a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1v1a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-1a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1v-.5A4.5 4.5 0 0 0 10.5 3h-2zM14 7.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V7.5A3.5 3.5 0 0 1 5.5 4h5A3.5 3.5 0 0 1 14 7.5"/>
</svg>

After

Width:  |  Height:  |  Size: 996 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

BIN
web/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB