web last fixes #335
@@ -53,7 +53,7 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
|
|||||||
|
|
||||||
List<Appointment> findByPet_Id(Long petId);
|
List<Appointment> findByPet_Id(Long petId);
|
||||||
|
|
||||||
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.petId = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')")
|
@Query("SELECT a FROM Appointment a JOIN FETCH a.service WHERE a.pet.id = :petId AND a.appointmentDate = :date AND LOWER(a.appointmentStatus) NOT IN ('cancelled', 'missed')")
|
||||||
List<Appointment> findByPetIdAndAppointmentDate(@Param("petId") Long petId, @Param("date") LocalDate date);
|
List<Appointment> findByPetIdAndAppointmentDate(@Param("petId") Long petId, @Param("date") LocalDate date);
|
||||||
|
|
||||||
List<Appointment> findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status);
|
List<Appointment> findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status);
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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([]);
|
||||||
@@ -496,8 +508,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>
|
||||||
@@ -517,7 +529,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>
|
||||||
@@ -540,7 +555,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>
|
||||||
@@ -552,10 +570,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>}
|
||||||
@@ -568,9 +586,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"}
|
||||||
@@ -581,14 +599,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>
|
||||||
)}
|
)}
|
||||||
@@ -605,7 +623,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>
|
||||||
@@ -622,7 +640,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,
|
||||||
@@ -653,7 +671,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" }} />
|
||||||
@@ -790,7 +808,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,
|
||||||
@@ -798,8 +815,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",
|
||||||
@@ -899,7 +915,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",
|
||||||
@@ -920,6 +936,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",
|
||||||
@@ -951,8 +968,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",
|
||||||
@@ -995,8 +1012,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",
|
||||||
@@ -1103,6 +1120,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",
|
||||||
|
|||||||
@@ -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'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'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're a first-time pet owner or a seasoned animal lover, we're here to help every step of the way.</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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" }} />
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
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 |