Comments, appointments adjustments, fixed some issues

This commit is contained in:
augmentedpotato
2026-04-20 19:19:30 -06:00
parent 1523aef51e
commit 6d593726a5
34 changed files with 402 additions and 104 deletions

View File

@@ -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>

View File

@@ -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,

View File

@@ -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]">

View File

@@ -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}`}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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; }

View File

@@ -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>

View File

@@ -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 "";