Comments, appointments adjustments, fixed some issues
This commit is contained in:
@@ -5,6 +5,7 @@ import { CartProvider } from "@/context/CartContext";
|
||||
import { ChatWidgetProvider } from "@/context/ChatWidgetContext";
|
||||
import FloatingChat from "@/components/FloatingChat";
|
||||
|
||||
//Wraps the app in all client-side context providers and adds the floating chat button
|
||||
export default function ClientProviders({ children }) {
|
||||
return (
|
||||
<AuthProvider>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { usePathname } from "next/navigation";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useChatWidget } from "@/context/ChatWidgetContext";
|
||||
|
||||
//Floating chat button and popup window - handles AI chat, live support, and conversation history
|
||||
export default function FloatingChat() {
|
||||
const pathname = usePathname();
|
||||
const { user, token } = useAuth();
|
||||
@@ -25,6 +26,7 @@ export default function FloatingChat() {
|
||||
const prevAiLengthRef = useRef(0);
|
||||
const prevLiveLengthRef = useRef(0);
|
||||
|
||||
//Scrolls to the bottom when new messages arrive, but only if already near the bottom
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
const aiGrew = aiMessages.length > prevAiLengthRef.current;
|
||||
@@ -41,12 +43,13 @@ const prevLiveLengthRef = useRef(0);
|
||||
if (view === "history" && token && isOpen) loadConversations(token);
|
||||
}, [view, token, isOpen, loadConversations]);
|
||||
|
||||
// Hide widget on dedicated chat pages
|
||||
//Don't show the widget on the full chat pages since they have their own UI
|
||||
if (pathname === "/ai-chat" || pathname === "/chat") return null;
|
||||
|
||||
const openConvCount = conversations.filter((c) => c.status === "OPEN").length;
|
||||
const isLiveClosed = activeConv?.status === "CLOSED";
|
||||
|
||||
//Sends the typed message to whichever chat is currently active
|
||||
async function handleSend(e) {
|
||||
e?.preventDefault();
|
||||
const text = input.trim();
|
||||
@@ -358,7 +361,7 @@ const prevLiveLengthRef = useRef(0);
|
||||
);
|
||||
}
|
||||
|
||||
// Styles
|
||||
//Inline style objects for the floating chat widget
|
||||
const s = {
|
||||
fab: {
|
||||
position: "fixed", bottom: 24, right: 24,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
//Shared CSS class for footer links
|
||||
const linkCls = "text-[#2f2f2f] no-underline text-[0.95rem] opacity-85 transition-opacity hover:opacity-100 hover:underline";
|
||||
|
||||
//Site footer with quick links, company links, and contact info
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-[#e68672] text-[#2f2f2f] mt-16 rounded-t-[10px]">
|
||||
|
||||
@@ -7,11 +7,13 @@ import { useEffect, useState } from "react";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useCart } from "@/context/CartContext";
|
||||
|
||||
//Shared CSS class strings for nav links and buttons
|
||||
const drawerLinkCls = "block text-[#2f2f2f] no-underline text-[1.05rem] font-medium px-2 py-[0.65rem] rounded-md transition-colors hover:bg-[rgba(47,47,47,0.1)]";
|
||||
const navLinkCls = "text-[#2f2f2f] no-underline text-[1.05rem] font-semibold px-4 py-2 rounded-md transition-all duration-[250ms] hover:bg-white/25";
|
||||
const cartBtnCls = "relative inline-flex items-center text-[1.4rem] no-underline mr-2 px-[0.4rem] py-[0.2rem] rounded-md transition-colors hover:bg-white/20";
|
||||
const cartBadgeCls = "absolute -top-1 -right-1.5 bg-[#e53935] text-white rounded-full text-[0.65rem] font-bold min-w-[18px] h-[18px] flex items-center justify-center px-[3px] leading-none";
|
||||
|
||||
//Cart icon with a red badge showing the number of items
|
||||
function CartIcon({ itemCount, onClick }) {
|
||||
return (
|
||||
<Link href="/cart" className={`${cartBtnCls} group`} aria-label="Cart" onClick={onClick}>
|
||||
@@ -26,6 +28,7 @@ function CartIcon({ itemCount, onClick }) {
|
||||
);
|
||||
}
|
||||
|
||||
//Top navigation bar - desktop links on the left, store selector and auth on the right, hamburger menu on mobile
|
||||
export default function DisplayNav() {
|
||||
const { user, logout, loading } = useAuth();
|
||||
const { itemCount, selectedStoreId, setStoreId } = useCart();
|
||||
@@ -33,6 +36,7 @@ export default function DisplayNav() {
|
||||
const [stores, setStores] = useState([]);
|
||||
const [menuOpen, setMenuOpen] = useState(false);
|
||||
|
||||
//Loads the store list for the store selector dropdown
|
||||
useEffect(() => {
|
||||
fetch("/api/v1/stores?size=100")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
@@ -40,6 +44,7 @@ export default function DisplayNav() {
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
//Logs out and sends the user to the home page
|
||||
function handleLogout() {
|
||||
logout();
|
||||
router.push("/");
|
||||
@@ -48,6 +53,7 @@ export default function DisplayNav() {
|
||||
|
||||
function closeMenu() { setMenuOpen(false); }
|
||||
|
||||
//Store selector dropdown, shared between desktop and mobile layouts
|
||||
const storeSelect = (extraCls = "") => stores.length > 0 && (
|
||||
<select
|
||||
className={`bg-[rgba(47,47,47,0.1)] text-[#2f2f2f] border border-[rgba(47,47,47,0.35)] rounded-md px-[0.6rem] py-[0.3rem] text-[0.9rem] cursor-pointer outline-none transition-colors hover:bg-[rgba(47,47,47,0.2)] ${extraCls}`}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from "next/link";
|
||||
import { getStatusClass } from "@/components/petUtils";
|
||||
|
||||
//Card shown in the adopt grid, links to the pet's detail page
|
||||
export default function PetCard({petId, petName, petSpecies, petStatus, imageUrl}) {
|
||||
return (
|
||||
<Link href={`/adopt/${petId}`} className="no-underline text-inherit flex flex-col rounded-2xl overflow-hidden shadow-[0_4px_12px_rgba(0,0,0,0.08)] transition-all duration-300 hover:-translate-y-1.5 hover:shadow-[0_8px_24px_rgba(0,0,0,0.13)] bg-white">
|
||||
|
||||
@@ -5,6 +5,7 @@ const fieldRowCls = "flex items-center px-5 py-[0.85rem] border-b border-[#eee]
|
||||
const fieldLabelCls = "w-[140px] text-[0.9rem] font-semibold text-[#888] uppercase tracking-[0.04em] shrink-0";
|
||||
const fieldValueCls = "text-base text-[#333]";
|
||||
|
||||
//Full detail view for a single pet, shown on the adopt detail page
|
||||
export default function PetProfile({ petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, storeId, storeName }) {
|
||||
return (
|
||||
<div className="flex gap-12 bg-white rounded-2xl shadow-[0_6px_24px_rgba(0,0,0,0.1)] overflow-hidden max-[768px]:flex-col max-[768px]:gap-0">
|
||||
@@ -31,22 +32,22 @@ export default function PetProfile({ petId, petName, petSpecies, petBreed, petAg
|
||||
<div className="flex flex-col border border-[#eee] rounded-[10px] overflow-hidden">
|
||||
<div className={fieldRowCls}>
|
||||
<span className={fieldLabelCls}>Species</span>
|
||||
<span className={fieldValueCls}>{petSpecies ?? "—"}</span>
|
||||
<span className={fieldValueCls}>{petSpecies ?? "-"}</span>
|
||||
</div>
|
||||
<div className={fieldRowCls}>
|
||||
<span className={fieldLabelCls}>Breed</span>
|
||||
<span className={fieldValueCls}>{petBreed ?? "—"}</span>
|
||||
<span className={fieldValueCls}>{petBreed ?? "-"}</span>
|
||||
</div>
|
||||
<div className={fieldRowCls}>
|
||||
<span className={fieldLabelCls}>Age</span>
|
||||
<span className={fieldValueCls}>
|
||||
{petAge != null ? `${petAge} ${petAge === 1 ? "year" : "years"}` : "—"}
|
||||
{petAge != null ? `${petAge} ${petAge === 1 ? "year" : "years"}` : "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={fieldRowCls}>
|
||||
<span className={fieldLabelCls}>Adoption Fee</span>
|
||||
<span className={`${fieldValueCls} font-bold text-[#1a7a3c] text-[1.1rem]`}>
|
||||
{petPrice != null ? `$${parseFloat(petPrice).toFixed(2)}` : "—"}
|
||||
{petPrice != null ? `$${parseFloat(petPrice).toFixed(2)}` : "-"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,8 +6,10 @@ import { useRouter } from "next/navigation";
|
||||
import { useAuth } from "@/context/AuthContext";
|
||||
import { useCart } from "@/context/CartContext";
|
||||
|
||||
//Shared CSS class for the quantity plus and minus buttons
|
||||
const qtyBtnCls = "w-7 h-7 border border-[#ddd] rounded-md bg-white text-base cursor-pointer flex items-center justify-center transition-colors hover:border-[#e68672] disabled:opacity-50";
|
||||
|
||||
//Card shown in the products grid, includes quantity selector and add to cart button
|
||||
export default function ProductCard({ prodId, prodName, categoryName, prodPrice, imageUrl }) {
|
||||
const { user } = useAuth();
|
||||
const { addItem, selectedStoreId } = useCart();
|
||||
@@ -16,6 +18,7 @@ export default function ProductCard({ prodId, prodName, categoryName, prodPrice,
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
|
||||
//Adds the selected quantity to the cart, redirects to login if not logged in
|
||||
async function handleAddToCart(e) {
|
||||
e.preventDefault();
|
||||
if (!user) { router.push("/login"); return; }
|
||||
|
||||
@@ -9,6 +9,7 @@ const fieldLabelCls = "w-[140px] text-[0.9rem] font-semibold text-[#888] upperca
|
||||
const fieldValueCls = "text-base text-[#333]";
|
||||
const qtyBtnCls = "w-7 h-7 border border-[#ddd] rounded-md bg-white text-base cursor-pointer flex items-center justify-center transition-colors hover:border-[#e68672] disabled:opacity-50";
|
||||
|
||||
//Full detail view for a single product, shown on the product detail page
|
||||
export default function ProductProfile({ prodId, prodName, categoryName, prodDesc, prodPrice, imageUrl }) {
|
||||
const { user } = useAuth();
|
||||
const { addItem, selectedStoreId } = useCart();
|
||||
@@ -17,8 +18,10 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes
|
||||
const [adding, setAdding] = useState(false);
|
||||
const [feedback, setFeedback] = useState(null);
|
||||
|
||||
//Increments or decrements quantity, minimum of 1
|
||||
function changeQty(delta) { setQuantity((q) => Math.max(1, q + delta)); }
|
||||
|
||||
//Adds the chosen quantity to the cart and shows a success or error message
|
||||
async function handleAddToCart() {
|
||||
setAdding(true);
|
||||
setFeedback(null);
|
||||
@@ -56,17 +59,17 @@ export default function ProductProfile({ prodId, prodName, categoryName, prodDes
|
||||
<div className="flex flex-col border border-[#eee] rounded-[10px] overflow-hidden">
|
||||
<div className={fieldRowCls}>
|
||||
<span className={fieldLabelCls}>Category</span>
|
||||
<span className={fieldValueCls}>{categoryName ?? "—"}</span>
|
||||
<span className={fieldValueCls}>{categoryName ?? "-"}</span>
|
||||
</div>
|
||||
<div className={fieldRowCls}>
|
||||
<span className={fieldLabelCls}>Price</span>
|
||||
<span className={`${fieldValueCls} font-bold text-[#1a7a3c] text-[1.1rem]`}>
|
||||
{prodPrice != null ? `$${parseFloat(prodPrice).toFixed(2)}` : "—"}
|
||||
{prodPrice != null ? `$${parseFloat(prodPrice).toFixed(2)}` : "-"}
|
||||
</span>
|
||||
</div>
|
||||
<div className={fieldRowCls}>
|
||||
<span className={fieldLabelCls}>Description</span>
|
||||
<span className={fieldValueCls}>{prodDesc ?? "—"}</span>
|
||||
<span className={fieldValueCls}>{prodDesc ?? "-"}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export const SPECIES_EMOJI = {
|
||||
guinea: "🐹",
|
||||
};
|
||||
|
||||
//Returns an emoji for a given species name, falls back to a paw print
|
||||
export function getSpeciesEmoji(species) {
|
||||
if (!species) {
|
||||
|
||||
@@ -31,6 +32,7 @@ export function getSpeciesEmoji(species) {
|
||||
return "🐾";
|
||||
}
|
||||
|
||||
//Returns the CSS class name for a pet's status badge
|
||||
export function getStatusClass(status) {
|
||||
if (!status) {
|
||||
return "";
|
||||
|
||||
Reference in New Issue
Block a user