Merge pull request #335 from RecentRunner/web-lastfixes
web last fixes
@@ -131,7 +131,7 @@ export default function AdoptPage() {
|
||||
<input
|
||||
className={`${inputCls} flex-1 max-w-[400px] font-[inherit] max-[600px]:max-w-full`}
|
||||
type="text"
|
||||
placeholder="Search by name, species, or breed..."
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -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() {
|
||||
const { user, token, loading: authLoading } = useAuth();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const conversationIdParam = searchParams.get("id");
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const [conversation, setConversation] = useState(null);
|
||||
const [messages, setMessages] = useState([]);
|
||||
@@ -498,8 +510,8 @@ function AiChatPage() {
|
||||
</section>
|
||||
|
||||
<section style={s.chatSection}>
|
||||
<div style={{ display: "flex", gap: "1rem", alignItems: "flex-start" }}>
|
||||
<div style={s.sidebar}>
|
||||
<div style={{ display: "flex", flexDirection: isMobile ? "column" : "row", gap: "1rem", alignItems: "flex-start" }}>
|
||||
<div style={{ ...s.sidebar, width: isMobile ? "100%" : 230, maxHeight: isMobile ? 260 : "calc(100vh - 220px)" }}>
|
||||
<div style={s.sidebarHeader}>
|
||||
<span style={s.sidebarTitle}>All Conversations</span>
|
||||
</div>
|
||||
@@ -519,7 +531,10 @@ function AiChatPage() {
|
||||
<span style={{ ...s.convStatusBadge, ...s.convStatusOpen }}>{conv.status}</span>
|
||||
</div>
|
||||
<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>
|
||||
</div>
|
||||
</button>
|
||||
@@ -542,7 +557,10 @@ function AiChatPage() {
|
||||
<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.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>
|
||||
</div>
|
||||
</button>
|
||||
@@ -554,10 +572,10 @@ function AiChatPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{ flex: 1, minWidth: 0, width: isMobile ? "100%" : undefined }}>
|
||||
{!conversation ? (
|
||||
<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>
|
||||
<p style={s.noConvText}>Start a new conversation with the AI assistant.</p>
|
||||
{error && <div style={s.errorInline}>{error}</div>}
|
||||
@@ -570,9 +588,9 @@ function AiChatPage() {
|
||||
</div>
|
||||
) : (
|
||||
<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={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 style={s.chatHeaderTitle}>
|
||||
{isEscalated ? (hasStaff ? "Support Agent" : "Leon's Pet Store Support") : "Leon's Pet Assistant"}
|
||||
@@ -583,14 +601,14 @@ function AiChatPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: "flex", gap: "0.5rem" }}>
|
||||
<div style={{ display: "flex", gap: "0.5rem", width: isMobile ? "100%" : undefined }}>
|
||||
{!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
|
||||
</button>
|
||||
)}
|
||||
{!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
|
||||
</button>
|
||||
)}
|
||||
@@ -607,7 +625,7 @@ function AiChatPage() {
|
||||
<div style={s.messagesArea} ref={messagesAreaRef}>
|
||||
{messages.length === 0 && (
|
||||
<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}>
|
||||
{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>
|
||||
@@ -624,7 +642,7 @@ function AiChatPage() {
|
||||
...(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
|
||||
style={{
|
||||
...s.messageBubble,
|
||||
@@ -655,7 +673,7 @@ function AiChatPage() {
|
||||
|
||||
{botTyping && !isEscalated && (
|
||||
<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" }}>
|
||||
<span className="fc-dot" />
|
||||
<span className="fc-dot" style={{ animationDelay: "0.2s" }} />
|
||||
@@ -792,7 +810,6 @@ const s = {
|
||||
padding: "1.5rem 1rem 2rem",
|
||||
},
|
||||
sidebar: {
|
||||
width: 230,
|
||||
flexShrink: 0,
|
||||
background: "white",
|
||||
borderRadius: 16,
|
||||
@@ -800,8 +817,7 @@ const s = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
maxHeight: "calc(100vh - 220px)",
|
||||
minHeight: 300,
|
||||
minHeight: 200,
|
||||
},
|
||||
sidebarHeader: {
|
||||
display: "flex",
|
||||
@@ -901,7 +917,7 @@ const s = {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "calc(100vh - 220px)",
|
||||
minHeight: 450,
|
||||
minHeight: 400,
|
||||
},
|
||||
chatHeader: {
|
||||
display: "flex",
|
||||
@@ -922,6 +938,7 @@ const s = {
|
||||
height: 44,
|
||||
borderRadius: "50%",
|
||||
background: "linear-gradient(135deg, #444, #666)",
|
||||
border: "3px solid #666",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@@ -953,8 +970,8 @@ const s = {
|
||||
border: "2px solid #ff8c00",
|
||||
color: "#ff8c00",
|
||||
borderRadius: 8,
|
||||
padding: "0.45rem 0.9rem",
|
||||
fontSize: "0.82rem",
|
||||
padding: "0.3rem 0.65rem",
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
whiteSpace: "nowrap",
|
||||
@@ -997,8 +1014,8 @@ const s = {
|
||||
border: "2px solid #c0392b",
|
||||
color: "#c0392b",
|
||||
borderRadius: 8,
|
||||
padding: "0.45rem 0.9rem",
|
||||
fontSize: "0.82rem",
|
||||
padding: "0.3rem 0.65rem",
|
||||
fontSize: "0.72rem",
|
||||
fontWeight: 600,
|
||||
cursor: "pointer",
|
||||
whiteSpace: "nowrap",
|
||||
@@ -1105,6 +1122,7 @@ const s = {
|
||||
height: 30,
|
||||
borderRadius: "50%",
|
||||
background: "linear-gradient(135deg, #444, #666)",
|
||||
border: "3px solid #666",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
@@ -66,11 +66,11 @@ export default function Home() {
|
||||
</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="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'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 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">
|
||||
<li>Support responsible pet adoption</li>
|
||||
<li>Provide grooming and care services</li>
|
||||
@@ -79,7 +79,7 @@ export default function Home() {
|
||||
</ul>
|
||||
</div>
|
||||
<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're a first-time pet owner or a seasoned animal lover, we're here to help every step of the way.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function ProductsPage() {
|
||||
<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]"
|
||||
type="text"
|
||||
placeholder="Search by name or category..."
|
||||
placeholder="Search"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
@@ -444,7 +444,7 @@ export default function ProfilePage() {
|
||||
{fields.map(({ label, value }) => (
|
||||
<div key={label} className="flex gap-2 py-1">
|
||||
<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>
|
||||
))}
|
||||
</dl>
|
||||
@@ -552,7 +552,7 @@ export default function ProfilePage() {
|
||||
</select>
|
||||
</label>
|
||||
<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"}
|
||||
</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}>
|
||||
|
||||
@@ -20,6 +20,7 @@ export default function FloatingChat() {
|
||||
} = useChatWidget();
|
||||
|
||||
const [input, setInput] = useState("");
|
||||
const [fabHovered, setFabHovered] = useState(false);
|
||||
const messagesEndRef = useRef(null);
|
||||
const prevAiLengthRef = useRef(0);
|
||||
const prevLiveLengthRef = useRef(0);
|
||||
@@ -65,8 +66,15 @@ const prevLiveLengthRef = useRef(0);
|
||||
return (
|
||||
<>
|
||||
{/* Floating toggle button */}
|
||||
<button onClick={toggleOpen} style={s.fab} aria-label={isOpen ? "Close chat" : "Open chat"}>
|
||||
<span style={{ fontSize: "1.4rem", lineHeight: 1 }}>{isOpen ? "✕" : "💬"}</span>
|
||||
<button onClick={toggleOpen} style={s.fab} aria-label={isOpen ? "Close chat" : "Open chat"} onMouseEnter={() => setFabHovered(true)} onMouseLeave={() => setFabHovered(false)}>
|
||||
{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>}
|
||||
</button>
|
||||
|
||||
@@ -77,7 +85,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
{/* Header */}
|
||||
<div style={s.header}>
|
||||
<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 style={s.headerTitle}>Leon's Assistant</div>
|
||||
<div style={s.headerSub}>
|
||||
@@ -99,7 +107,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
{/* Guest */}
|
||||
{!user && (
|
||||
<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" }}>
|
||||
Log in to chat with our pet assistant!
|
||||
</p>
|
||||
@@ -129,7 +137,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
|
||||
{!convsLoading && conversations.length === 0 && (
|
||||
<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>
|
||||
</div>
|
||||
)}
|
||||
@@ -223,7 +231,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
return (
|
||||
<div key={msg.id} style={{ ...s.row, ...(isUser ? s.rowUser : s.rowOther) }}>
|
||||
{!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) }}>
|
||||
{!isUser && (
|
||||
@@ -282,7 +290,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
<div style={s.messages}>
|
||||
{aiMessages.length === 0 && (
|
||||
<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}>
|
||||
Hi{user?.fullName ? `, ${user.fullName.split(" ")[0]}` : ""}!<br />
|
||||
Ask me anything about pets.
|
||||
@@ -292,7 +300,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
|
||||
{aiMessages.map((msg) => (
|
||||
<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) }}>
|
||||
{msg.content.split("\n").map((line, i, arr) => (
|
||||
<span key={i}>{line}{i < arr.length - 1 && <br />}</span>
|
||||
@@ -308,7 +316,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
|
||||
{aiSending && (
|
||||
<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 }}>
|
||||
<span className="fc-dot" />
|
||||
<span className="fc-dot" style={{ animationDelay: "0.2s" }} />
|
||||
|
||||
@@ -14,8 +14,11 @@ const cartBadgeCls = "absolute -top-1 -right-1.5 bg-[#e53935] text-white rounded
|
||||
|
||||
function CartIcon({ itemCount, onClick }) {
|
||||
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 && (
|
||||
<span className={cartBadgeCls}>{itemCount > 99 ? "99+" : itemCount}</span>
|
||||
)}
|
||||
@@ -59,7 +62,7 @@ export default function DisplayNav() {
|
||||
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">
|
||||
<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>
|
||||
|
||||
{/* Desktop nav links */}
|
||||
|
||||
@@ -26,6 +26,7 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes
|
||||
await addItem(prodId, quantity);
|
||||
setFeedback({ type: "success", message: `${quantity} × ${prodName} added to cart!` });
|
||||
setQuantity(1);
|
||||
setTimeout(() => setFeedback(null), 1000);
|
||||
} catch (err) {
|
||||
setFeedback({ type: "error", message: err.message ?? "Failed to add to cart." });
|
||||
} finally {
|
||||
@@ -90,15 +91,17 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes
|
||||
</div>
|
||||
|
||||
<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}
|
||||
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"}
|
||||
</button>
|
||||
|
||||
{feedback && (
|
||||
<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]"}`}>
|
||||
{feedback?.type === "error" && (
|
||||
<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}
|
||||
</p>
|
||||
)}
|
||||
|
||||
3
web/public/bootstrap/cart-fill.svg
Normal 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 |
3
web/public/bootstrap/cart-plus-fill.svg
Normal 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 |
3
web/public/bootstrap/cart.svg
Normal 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 |
3
web/public/bootstrap/chat-fill.svg
Normal 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 |
3
web/public/bootstrap/chat.svg
Normal 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 |
4
web/public/bootstrap/person-circle.svg
Normal 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 |
3
web/public/bootstrap/person-fill.svg
Normal 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 |
4
web/public/bootstrap/robot.svg
Normal 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 |
|
Before Width: | Height: | Size: 103 KiB |
BIN
web/public/logo.png
Normal file
|
After Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 103 KiB |