diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000..739d260a --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(grep -E \"\\\\.\\(tsx?|jsx?\\)$\")" + ] + } +} diff --git a/backend/src/main/java/com/petshop/backend/controller/CartController.java b/backend/src/main/java/com/petshop/backend/controller/CartController.java index 49281666..6887326c 100644 --- a/backend/src/main/java/com/petshop/backend/controller/CartController.java +++ b/backend/src/main/java/com/petshop/backend/controller/CartController.java @@ -91,4 +91,13 @@ public class CartController { return ResponseEntity.noContent().build(); } + + @PostMapping("/checkout/cancel") + @PreAuthorize("isAuthenticated()") + public ResponseEntity cancelCheckout(@RequestParam Long storeId) { + Long userId = AuthenticationHelper.getAuthenticatedUserId(); + cartService.cancelCheckout(userId, storeId); + + return ResponseEntity.noContent().build(); + } } diff --git a/backend/src/main/java/com/petshop/backend/dto/cart/CartResponse.java b/backend/src/main/java/com/petshop/backend/dto/cart/CartResponse.java index d5fc9faf..3e0c2919 100644 --- a/backend/src/main/java/com/petshop/backend/dto/cart/CartResponse.java +++ b/backend/src/main/java/com/petshop/backend/dto/cart/CartResponse.java @@ -16,6 +16,7 @@ public class CartResponse { private String couponCode; private Boolean pointsApplied; private Integer availableLoyaltyPoints; + private Boolean checkoutPending; public CartResponse() { } @@ -53,4 +54,7 @@ public class CartResponse { public Integer getAvailableLoyaltyPoints() { return availableLoyaltyPoints; } public void setAvailableLoyaltyPoints(Integer availableLoyaltyPoints) { this.availableLoyaltyPoints = availableLoyaltyPoints; } + public Boolean getCheckoutPending() { return checkoutPending; } + public void setCheckoutPending(Boolean checkoutPending) { this.checkoutPending = checkoutPending; } + } diff --git a/backend/src/main/java/com/petshop/backend/service/CartService.java b/backend/src/main/java/com/petshop/backend/service/CartService.java index 1714f518..76543163 100644 --- a/backend/src/main/java/com/petshop/backend/service/CartService.java +++ b/backend/src/main/java/com/petshop/backend/service/CartService.java @@ -482,15 +482,21 @@ public class CartService { List itemResponses = cartItemRepository .findByCartCartId(cart.getCartId()) .stream() - .map(item -> new CartItemResponse( - item.getCartItemId(), - item.getProduct().getProdId(), - item.getProduct().getProdName(), - item.getProduct().getImageUrl(), - item.getUnitPrice(), - item.getQuantity(), - item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity())) - )) + .map(item -> { + String rawImageUrl = item.getProduct().getImageUrl(); + String imageUrl = (rawImageUrl != null && !rawImageUrl.isBlank()) + ? "/api/v1/products/" + item.getProduct().getProdId() + "/image" + : null; + return new CartItemResponse( + item.getCartItemId(), + item.getProduct().getProdId(), + item.getProduct().getProdName(), + imageUrl, + item.getUnitPrice(), + item.getQuantity(), + item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity())) + ); + }) .toList(); CartResponse response = new CartResponse(); @@ -505,7 +511,23 @@ public class CartService { response.setCouponCode(cart.getCoupon() != null ? cart.getCoupon().getCouponCode() : null); response.setPointsApplied(cart.getPointsApplied()); response.setAvailableLoyaltyPoints(cart.getUser() != null ? cart.getUser().getLoyaltyPoints() : null); - + response.setCheckoutPending(cart.getCheckoutPending()); + return response; } + + @Transactional + public void cancelCheckout(Long userId, Long storeId) { + cartRepository.findActiveCartByUserAndStore(userId, storeId, "ACTIVE") + .ifPresent(cart -> { + if (!Boolean.TRUE.equals(cart.getCheckoutPending())) { + return; + } + cart.setCheckoutPending(false); + cart.setCheckoutAmount(null); + cart.setCheckoutStartedAt(null); + cart.setCheckoutPaymentIntentId(null); + cartRepository.save(cart); + }); + } } diff --git a/web/app/adopt/page.js b/web/app/adopt/page.js index 2d4e7a18..3f364f94 100644 --- a/web/app/adopt/page.js +++ b/web/app/adopt/page.js @@ -15,7 +15,6 @@ export default function AdoptPage() { const [pets, setPets] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [health, setHealth] = useState(null); const [search, setSearch] = useState(""); const [query, setQuery] = useState(""); @@ -26,16 +25,9 @@ export default function AdoptPage() { // Species options come from a dedicated fetch (only store-filtered, no species filter) const [speciesOptions, setSpeciesOptions] = useState([]); - // ---------- health check ---------- - useEffect(() => { - fetch(`${API_BASE}/api/v1/health`) - .then((res) => (res.ok ? setHealth("online") : setHealth("error"))) - .catch(() => setHealth("offline")); - }, []); - useEffect(() => { setSelectedSpecies(""); - const params = new URLSearchParams({ status: "Available", page: "0", size: String(PAGE_SIZE) }); + const params = new URLSearchParams({ page: "0", size: String(PAGE_SIZE) }); if (selectedStoreId) params.set("storeId", String(selectedStoreId)); fetch(`${API_BASE}/api/v1/pets?${params}`) @@ -62,7 +54,6 @@ export default function AdoptPage() { page: String(page), size: String(PAGE_SIZE), sort: "id,asc", - status: "Available", }); if (query) params.set("q", query); if (selectedSpecies) params.set("species", selectedSpecies); @@ -161,15 +152,6 @@ export default function AdoptPage() { Clear Filters )} - - @@ -180,13 +162,7 @@ export default function AdoptPage() {

Failed to load pets

{error} -

- {health === "offline" - ? "The Spring Boot backend is not reachable. Make sure it is running in IntelliJ on port 8080." - : health === "error" - ? "The backend responded with an error. Check the IntelliJ Run console for stack traces." - : "The backend is reachable but the /pets endpoint failed. Check the IntelliJ Run console."} -

+

Make sure the backend is running and try again.

)} diff --git a/web/app/cart/page.js b/web/app/cart/page.js index 5be185a8..e555a5b2 100644 --- a/web/app/cart/page.js +++ b/web/app/cart/page.js @@ -85,6 +85,7 @@ export default function CartPage() { clearCart, applyCoupon, checkout, + cancelCheckout, } = useCart(); const router = useRouter(); @@ -114,6 +115,14 @@ export default function CartPage() { } }, [cart]); + // If the cart arrives already locked (e.g. user closed the page mid-checkout) + // and there is no active Stripe session, release the lock automatically. + useEffect(() => { + if (cart?.checkoutPending && !clientSecret) { + cancelCheckout().catch(() => {}); + } + }, [cart?.checkoutPending, clientSecret, cancelCheckout]); + async function handleQuantityChange(cartItemId, newQty) { if (newQty < 1) { return; @@ -351,7 +360,10 @@ export default function CartPage() { setClientSecret(null); setConfirmed(true); }} - onCancel={() => setClientSecret(null)} + onCancel={async () => { + await cancelCheckout().catch(() => {}); + setClientSecret(null); + }} /> )} diff --git a/web/app/globals.css b/web/app/globals.css index c468269c..25de93db 100644 --- a/web/app/globals.css +++ b/web/app/globals.css @@ -30,13 +30,14 @@ body { top: 0; left: 0; width: 100%; - background: orange; + background: #e68672; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); z-index: 1000; padding: 0.5rem 2rem; display: flex; align-items: center; - height: 70px; + flex-wrap: wrap; + min-height: 70px; border-radius: 0px 0px 10px 10px; } @@ -62,12 +63,15 @@ body { display: flex; align-items: center; gap: 2rem; - margin-left: 2rem; + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); } /* Indivdual Link Styles */ .nav-link { - color: rgb(255, 255, 255); + color: #2f2f2f; text-decoration: none; font-size: 1.1rem; padding: 0.5rem 1rem; @@ -365,7 +369,7 @@ body { display: flex; align-items: center; justify-content: center; - height: 160px; + aspect-ratio: 1 / 1; } .pet-card-emoji { @@ -380,31 +384,34 @@ body { } .pet-card-body { - padding: 1rem 1.25rem 1.25rem; + padding: 0.6rem 0.75rem 0.75rem; display: flex; flex-direction: column; - gap: 0.3rem; + gap: 0.2rem; } .pet-card-name { - font-size: 1.2rem; + font-size: 0.95rem; font-weight: 700; color: #222; margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .pet-card-species { - font-size: 0.95rem; + font-size: 0.8rem; color: #666; margin: 0; } .pet-card-status { display: inline-block; - margin-top: 0.4rem; - padding: 0.2rem 0.75rem; + margin-top: 0.2rem; + padding: 0.15rem 0.5rem; border-radius: 20px; - font-size: 0.8rem; + font-size: 0.7rem; font-weight: 600; text-transform: capitalize; width: fit-content; @@ -595,6 +602,71 @@ body { margin: 0 0 1rem; } +.product-qty-row { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.product-qty-label { + font-size: 0.95rem; + font-weight: 600; + color: #555; +} + +.product-qty-controls { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.product-add-to-cart-btn { + width: 100%; + padding: 0.85rem; + background: #e68672; + color: #2f2f2f; + border: none; + border-radius: 10px; + font-size: 1rem; + font-weight: 700; + cursor: pointer; + transition: background 0.2s ease, transform 0.1s ease; +} + +.product-add-to-cart-btn:hover:not(:disabled) { + background: #d4705e; + color: #fff; +} + +.product-add-to-cart-btn:active:not(:disabled) { + transform: scale(0.98); +} + +.product-add-to-cart-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +.product-cart-feedback { + margin: 0.75rem 0 0; + font-size: 0.9rem; + border-radius: 8px; + padding: 0.6rem 1rem; +} + +.product-cart-feedback--success { + background: #f0fff4; + border: 1px solid #b2dfdb; + color: #1a7a3c; +} + +.product-cart-feedback--error { + background: #fff0f0; + border: 1px solid #f5c6c6; + color: #c0392b; +} + .pet-detail-cta-btn { display: inline-block; padding: 0.65rem 1.5rem; @@ -733,8 +805,8 @@ body { } .adopt-grid { - grid-template-columns: repeat(2, 1fr); - gap: 1.25rem; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; } .pet-detail-card { @@ -754,7 +826,8 @@ body { @media (max-width: 480px) { .adopt-grid { - grid-template-columns: 1fr; + grid-template-columns: repeat(3, 1fr); + gap: 0.6rem; } .adopt-hero-title, @@ -884,29 +957,6 @@ body { gap: 1rem; } -.backend-status { - display: inline-block; - width: 12px; - height: 12px; - border-radius: 50%; - flex-shrink: 0; -} - -.backend-status--online { - background: #1a7a3c; -} - -.backend-status--offline { - background: #c0392b; -} - -.backend-status--error { - background: #e67e00; -} - -.backend-status--checking { - background: #bbb; -} .adopt-error-box { max-width: 640px; @@ -964,21 +1014,21 @@ body { } .nav-register-btn { - background: white; - color: orange !important; + background: #2f2f2f; + color: #e68672 !important; font-weight: 600; border-radius: 20px; padding: 0.4rem 1rem !important; } .nav-register-btn:hover { - background: #fff3e0 !important; + background: #444 !important; } .nav-logout-btn { - background: rgba(255, 255, 255, 0.2); - color: white; - border: 1px solid rgba(255, 255, 255, 0.6); + background: rgba(47, 47, 47, 0.15); + color: #2f2f2f; + border: 1px solid rgba(47, 47, 47, 0.4); border-radius: 20px; padding: 0.35rem 1rem; font-size: 0.95rem; @@ -988,7 +1038,7 @@ body { } .nav-logout-btn:hover { - background: rgba(255, 255, 255, 0.35); + background: rgba(47, 47, 47, 0.25); } /* Login/Register */ @@ -1896,12 +1946,12 @@ body { color: #333; } -/* ── Store Selector ──────────────────────────────────────── */ +/* Store Selector */ .nav-store-select { - background: rgba(255, 255, 255, 0.15); - color: white; - border: 1px solid rgba(255, 255, 255, 0.4); + background: rgba(47, 47, 47, 0.1); + color: #2f2f2f; + border: 1px solid rgba(47, 47, 47, 0.35); border-radius: 6px; padding: 0.3rem 0.6rem; font-size: 0.9rem; @@ -1912,15 +1962,15 @@ body { } .nav-store-select option { - background: #333; - color: white; + background: #fff; + color: #2f2f2f; } .nav-store-select:hover { - background: rgba(255, 255, 255, 0.25); + background: rgba(47, 47, 47, 0.2); } -/* ── Cart Badge ──────────────────────────────────────────── */ +/* Cart Badge */ .nav-cart-btn { position: relative; @@ -2422,3 +2472,355 @@ body { @keyframes bounce { 0%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-6px); } } @keyframes spin { to { transform: rotate(360deg); } } + +/* Footer */ +.site-footer { + background: #e68672; + color: #2f2f2f; + margin-top: 4rem; + border-radius: 10px 10px 0 0; +} + +.footer-container { + max-width: 1200px; + margin: 0 auto; + padding: 3rem 2rem 2rem; + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr; + gap: 2rem; +} + +.footer-logo { + border-radius: 50%; + margin-bottom: 0.75rem; +} + +.footer-tagline { + font-size: 0.95rem; + line-height: 1.5; + opacity: 0.9; + max-width: 260px; +} + +.footer-heading { + font-size: 1rem; + font-weight: 700; + margin-bottom: 1rem; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.footer-links { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.footer-links a { + color: #2f2f2f; + text-decoration: none; + font-size: 0.95rem; + opacity: 0.85; + transition: opacity 0.2s ease; +} + +.footer-links a:hover { + opacity: 1; + text-decoration: underline; +} + +.footer-contact li { + font-size: 0.95rem; + opacity: 0.85; +} + +.footer-bottom { + border-top: 1px solid rgba(47, 47, 47, 0.2); + text-align: center; + padding: 1rem 2rem; + font-size: 0.85rem; + opacity: 0.8; +} + +@media (max-width: 900px) { + .footer-container { + grid-template-columns: 1fr 1fr; + } + .footer-brand { + grid-column: 1 / -1; + } +} + +@media (max-width: 550px) { + .footer-container { + grid-template-columns: 1fr; + } +} + +/* Mobile / Responsive */ + +@media (max-width: 768px) { + .navbar { + padding: 0.5rem 1rem; + } +} + +/* Hamburger button – hidden on desktop */ +.nav-mobile-bar { + display: none; +} + +.nav-hamburger { + display: flex; + flex-direction: column; + justify-content: center; + gap: 5px; + width: 36px; + height: 36px; + background: transparent; + border: none; + cursor: pointer; + padding: 4px; + border-radius: 6px; + transition: background 0.2s ease; +} + +.nav-hamburger:hover { + background: rgba(47, 47, 47, 0.12); +} + +.nav-hamburger span { + display: block; + height: 2px; + width: 100%; + background: #2f2f2f; + border-radius: 2px; + transition: transform 0.25s ease, opacity 0.25s ease; + transform-origin: center; +} + +/* Animate bars to X when open */ +.nav-hamburger--open span:nth-child(1) { + transform: translateY(7px) rotate(45deg); +} +.nav-hamburger--open span:nth-child(2) { + opacity: 0; +} +.nav-hamburger--open span:nth-child(3) { + transform: translateY(-7px) rotate(-45deg); +} + +/* Mobile slide-down drawer */ +.nav-drawer { + display: none; +} + +@media (max-width: 768px) { + /* Show hamburger bar, hide desktop nav */ + .nav-mobile-bar { + display: flex; + align-items: center; + gap: 0.5rem; + margin-left: auto; + } + + .nav-links, + .nav-auth { + display: none; + } + + /* Drawer panel */ + .nav-drawer { + display: flex; + flex-direction: column; + position: absolute; + top: 70px; + left: 0; + width: 100%; + background: #e68672; + padding: 1rem 1.5rem 1.5rem; + z-index: 999; + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); + border-radius: 0 0 10px 10px; + gap: 0.25rem; + } + + .nav-drawer-link { + display: block; + color: #2f2f2f; + text-decoration: none; + font-size: 1.05rem; + font-weight: 500; + padding: 0.65rem 0.5rem; + border-radius: 6px; + transition: background 0.2s ease; + } + + .nav-drawer-link:hover { + background: rgba(47, 47, 47, 0.1); + } + + .nav-drawer-link--register { + margin-top: 0.25rem; + background: #2f2f2f; + color: #e68672 !important; + font-weight: 700; + border-radius: 20px; + padding: 0.6rem 1rem; + text-align: center; + } + + .nav-drawer-link--register:hover { + background: #444 !important; + } + + .nav-drawer-divider { + height: 1px; + background: rgba(47, 47, 47, 0.2); + margin: 0.5rem 0; + } + + .nav-store-select--drawer { + width: 100%; + margin: 0 0 0.25rem; + } + + .nav-logout-btn--drawer { + width: 100%; + margin-top: 0.25rem; + border-radius: 8px; + padding: 0.65rem; + font-size: 1rem; + } +} + +/* Cart item row – stack on small screens */ +@media (max-width: 600px) { + .cart-item-row { + grid-template-columns: 56px 1fr; + grid-template-rows: auto auto; + gap: 0.5rem 0.75rem; + } + + .cart-item-img { + width: 56px; + height: 56px; + grid-row: 1 / 3; + } + + .cart-item-details { + grid-column: 2; + grid-row: 1; + } + + .cart-item-qty-controls { + grid-column: 2; + grid-row: 2; + } + + .cart-item-line-total { + display: none; + } + + .cart-item-remove { + position: absolute; + top: 0.75rem; + right: 0; + } + + .cart-item-row { + position: relative; + padding-right: 2rem; + } +} + +/* Adopt filters - stack vertically on mobile */ +@media (max-width: 600px) { + .adopt-filters-row { + flex-direction: column; + align-items: stretch; + } + + .adopt-filter-group { + min-width: 0; + width: 100%; + } + + .adopt-filter-select { + width: 100%; + } + + .adopt-search-form { + flex-direction: column; + align-items: stretch; + } + + .adopt-search-input { + max-width: 100%; + } + + .adopt-search-btn, + .adopt-clear-btn { + width: 100%; + } +} + +/* Appointments - fix adopt grid on narrow screens */ +@media (max-width: 480px) { + .appt-adopt-grid { + grid-template-columns: 1fr; + } + + .appt-modal { + margin: 1rem; + padding: 1.25rem; + } + + .appt-slots-grid { + gap: 0.4rem; + } + + .appt-slot-btn { + padding: 0.5rem 0.75rem; + font-size: 0.9rem; + } +} + +/* Auth pages - reduce padding on very small screens */ +@media (max-width: 480px) { + .auth-card { + padding: 1.5rem 1.25rem; + } + + .auth-title { + font-size: 1.4rem; + } +} + +/* Profile - tighten padding on mobile */ +@media (max-width: 480px) { + .profile-card { + padding: 1.5rem 1.25rem; + } + + .profile-pets-section { + padding: 1.25rem; + } + + .profile-pets-header { + flex-wrap: wrap; + gap: 0.5rem; + } +} + +/* General – prevent horizontal overflow on all pages */ +html, body { + overflow-x: hidden; +} + +img, video, iframe { + max-width: 100%; +} diff --git a/web/app/layout.js b/web/app/layout.js index 0a50c8db..d1466ee6 100644 --- a/web/app/layout.js +++ b/web/app/layout.js @@ -1,6 +1,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import DisplayNav from "@/components/Navigation"; +import Footer from "@/components/Footer"; import ClientProviders from "@/components/ClientProviders"; export const metadata = { @@ -15,6 +16,7 @@ export default function RootLayout({children}) { {children} +
diff --git a/web/app/products/[id]/page.js b/web/app/products/[id]/page.js index 47a3059b..fb0b731f 100644 --- a/web/app/products/[id]/page.js +++ b/web/app/products/[id]/page.js @@ -41,6 +41,7 @@ export default function ProductDetailPage() { {!loading && !error && product && ( +
+ +
+ Leon's Pet Store logo +

+ Your neighbourhood pet store! +

+
+ +
+

Quick Links

+
    +
  • Home
  • +
  • Adopt a Pet
  • +
  • Online Store
  • +
  • Schedule an Appointment
  • +
  • AI Assistant
  • +
+
+ +
+

Company

+
    +
  • About Us
  • +
  • Contact Us
  • +
+
+ +
+

Contact

+
    +
  • (403) 123-4567
  • +
  • support@leonspetstore.com
  • +
  • 123 Street Street, Calgary, Alberta, Canada
  • +
+
+ +
+ +
+

© {new Date().getFullYear()} Leon's Pet Store. All rights reserved.

+
+
+ ); +} diff --git a/web/components/Navigation.js b/web/components/Navigation.js index a6e04101..afd1ea50 100644 --- a/web/components/Navigation.js +++ b/web/components/Navigation.js @@ -12,6 +12,7 @@ export default function DisplayNav() { const { itemCount, selectedStoreId, setStoreId } = useCart(); const router = useRouter(); const [stores, setStores] = useState([]); + const [menuOpen, setMenuOpen] = useState(false); useEffect(() => { if (!token) return; @@ -26,27 +27,35 @@ export default function DisplayNav() { function handleLogout() { logout(); router.push("/"); + setMenuOpen(false); + } + + function closeMenu() { + setMenuOpen(false); } return ( ); } diff --git a/web/components/ProductProfile.js b/web/components/ProductProfile.js index fda41420..3e397b14 100644 --- a/web/components/ProductProfile.js +++ b/web/components/ProductProfile.js @@ -1,6 +1,35 @@ -import Link from "next/link"; +"use client"; + +import { useState } from "react"; +import { useAuth } from "@/context/AuthContext"; +import { useCart } from "@/context/CartContext"; + +export default function ProductProfile({ prodId, prodName, categoryName, prodDesc, prodPrice, imageUrl }) { + const { user } = useAuth(); + const { addItem, selectedStoreId } = useCart(); + + const [quantity, setQuantity] = useState(1); + const [adding, setAdding] = useState(false); + const [feedback, setFeedback] = useState(null); // { type: "success"|"error", message } + + function changeQty(delta) { + setQuantity((q) => Math.max(1, q + delta)); + } + + async function handleAddToCart() { + setAdding(true); + setFeedback(null); + try { + await addItem(prodId, quantity); + setFeedback({ type: "success", message: `${quantity} × ${prodName} added to cart!` }); + setQuantity(1); + } catch (err) { + setFeedback({ type: "error", message: err.message ?? "Failed to add to cart." }); + } finally { + setAdding(false); + } + } -export default function ProductProfile({ prodName, categoryName, prodDesc, prodPrice, imageUrl }) { return (
@@ -36,6 +65,57 @@ export default function ProductProfile({ prodName, categoryName, prodDesc, prodP {prodDesc ?? "—"}
+ +
+ {!user ? ( +

+ Log in to purchase this item. +

+ ) : !selectedStoreId ? ( +

+ Select a store from the navigation bar to add items to your cart. +

+ ) : ( + <> +
+ Quantity +
+ + {quantity} + +
+
+ + + + {feedback && ( +

+ {feedback.message} +

+ )} + + )} +
); diff --git a/web/context/CartContext.js b/web/context/CartContext.js index d3dfc331..777da6ba 100644 --- a/web/context/CartContext.js +++ b/web/context/CartContext.js @@ -10,6 +10,7 @@ import { apiClearCart, apiApplyCoupon, apiCheckout, + apiCancelCheckout, } from "@/lib/cartApi"; const CartContext = createContext(null); @@ -133,6 +134,15 @@ export function CartProvider({ children }) { [token, selectedStoreId] ); + const cancelCheckout = useCallback( + async () => { + if (!token || !selectedStoreId) return; + await apiCancelCheckout(token, selectedStoreId); + await refreshCart(); + }, + [token, selectedStoreId, refreshCart] + ); + const itemCount = cart?.items?.reduce((sum, i) => sum + i.quantity, 0) ?? 0; return ( @@ -150,6 +160,7 @@ export function CartProvider({ children }) { clearCart, applyCoupon, checkout, + cancelCheckout, refreshCart, }} > diff --git a/web/lib/cartApi.js b/web/lib/cartApi.js index b47049bc..b8d608ce 100644 --- a/web/lib/cartApi.js +++ b/web/lib/cartApi.js @@ -91,3 +91,12 @@ export async function apiCompleteCheckout(token, paymentIntentId) { return handleResponse(res); } + +export async function apiCancelCheckout(token, storeId) { + const res = await fetch(`${BASE}/checkout/cancel?storeId=${storeId}`, { + method: "POST", + headers: authHeaders(token), + }); + + return handleResponse(res); +}