diff --git a/app/(main)/payment/page.tsx b/app/(main)/payment/page.tsx index 0888779..82b1634 100644 --- a/app/(main)/payment/page.tsx +++ b/app/(main)/payment/page.tsx @@ -9,8 +9,8 @@ import { useManager } from "@/lib/manager-context"; import { MenuItemEntity } from "@/lib/types"; import { useState } from "react"; -const formatPrice = (value: number) => - value.toLocaleString("vi-VN", { style: "currency", currency: "VND" }); +export const formatPrice = (value?: number) => + (value ?? 0).toLocaleString("vi-VN", { style: "currency", currency: "VND" }); export default function PaymentPage() { const { @@ -42,7 +42,7 @@ export default function PaymentPage() { - {items.length === 0 ? ( + {items?.length === 0 ? (
Chưa có sản phẩm nào trong giỏ hàng.
@@ -72,7 +72,7 @@ export default function PaymentPage() { - {items.map( + {items?.map( ({ productId: id, priceAtTimeOfAdding: price, diff --git a/components/molecules/cards/PaymentSummaryCard.tsx b/components/molecules/cards/PaymentSummaryCard.tsx index 68923ce..1ce88f6 100644 --- a/components/molecules/cards/PaymentSummaryCard.tsx +++ b/components/molecules/cards/PaymentSummaryCard.tsx @@ -1,3 +1,4 @@ +import { formatPrice } from "@/app/(main)/payment/page"; import Button from "@/components/atoms/buttons/Button"; import { ReviewModal } from "@/components/organisms/modals"; import Link from "next/link"; @@ -5,9 +6,6 @@ import { useState } from "react"; import type { PaymentSummaryCardProps } from "./Card.types"; -const formatPrice = (value: number) => - value.toLocaleString("vi-VN", { style: "currency", currency: "VND" }); - export default function PaymentSummaryCard({ totalPrice, isCustomer = false, diff --git a/components/molecules/cards/ProductCard.tsx b/components/molecules/cards/ProductCard.tsx index de0a79d..f18b27a 100644 --- a/components/molecules/cards/ProductCard.tsx +++ b/components/molecules/cards/ProductCard.tsx @@ -38,7 +38,7 @@ export default function ProductCard({ {/* Product image */} {imageAlt} { + const filteredProducts = products.filter((p) => { const isAvailable = p.available !== false; const matchesSearch = searchQuery.trim() === "" || @@ -30,17 +31,19 @@ export default function ProductGrid({ {/* ── Product grid ── */} {filteredProducts.length > 0 ? (
- {filteredProducts.map((product) => ( - addToCart(product)} - /> - ))} + {filteredProducts.map( + ({ id, imageUrl, name, price, description }) => ( + addToCart({ productId: id!, quantity: 1 })} + /> + ), + )}
) : ( /* Empty state */ diff --git a/lib/cart-context.tsx b/lib/cart-context.tsx index e75fcc4..a46fd57 100644 --- a/lib/cart-context.tsx +++ b/lib/cart-context.tsx @@ -1,131 +1,193 @@ "use client"; +import { gql } from "@apollo/client"; +import { useMutation, useQuery } from "@apollo/client/react"; import { createContext, useContext, useEffect, useMemo, useState } from "react"; -import { CartItemEntity, MenuItemEntity } from "./types"; +import { cartClient, eateryClient } from "./apollo-clients"; +import { + CartEntity, + CartItemEntity, + addMenuItemMutation, + allEateriesQuery, + createCartMutation, + getCartQuery, +} from "./types"; interface CartContextValue { items: CartItemEntity[]; totalItems: number; totalPrice: number; - addToCart: (product: MenuItemEntity) => void; + addToCart: (product: CartItemEntity) => void; increaseQty: (id: string) => void; decreaseQty: (id: string) => void; removeFromCart: (id: string) => void; setQuantity: (id: string, quantity: number) => void; } -const STORAGE_KEY = "coffee-shop-cart"; +const CART_ID = "cartId"; const CartContext = createContext(null); +const GET_CART_ITEMS = gql` + query getCart($cartId: String!) { + getCart(cartId: $cartId) { + Id + userId + eateryId + items { + productId + quantity + priceAtTimeOfAdding + subTotal + } + totalAmount + paymentQrUrl + } + } +`; + +const GET_EATERY = gql` + query GetEateryMenu { + allEateries { + id + } + } +`; + +const CREATE_CART = gql` + mutation createCart($eateryId: String!) { + createCart(eateryId: $eateryId) + } +`; + +const ADD_ITEM = gql` + mutation addItem( + $cartId: String! + $menuItemId: String! + $quantity: BigInteger! + ) { + addItem(cartId: $cartId, menuItemId: $menuItemId, quantity: $quantity) { + Id + userId + eateryId + items { + productId + quantity + priceAtTimeOfAdding + subTotal + } + totalAmount + paymentQrUrl + } + } +`; + export function CartProvider({ children }: { children: React.ReactNode }) { - const [items, setItems] = useState([]); + const [cart, setCart] = useState(null!); + const [cartId, setCartId] = useState(null); + + const { data: eateryData } = useQuery(GET_EATERY, { + client: eateryClient, + }); + + const [createCart] = useMutation(CREATE_CART, { + client: cartClient, + }); + + const { data, loading, error } = useQuery(GET_CART_ITEMS, { + client: cartClient, + variables: { cartId }, + skip: !cartId, + }); + + const [addMenuItem] = useMutation(ADD_ITEM, { + client: cartClient, + }); useEffect(() => { - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return; - const parsed = JSON.parse(raw) as CartItemEntity[]; - if (Array.isArray(parsed)) { - setItems(parsed.filter((i) => i && i.productId && i.quantity > 0)); + const createCartFunc = async () => { + if (eateryData && eateryData.allEateries?.length > 0) { + try { + const firstEateryId = eateryData.allEateries[0].id; + const { data: mutationResult } = await createCart({ + variables: { eateryId: firstEateryId }, + }); + + const newCartId = mutationResult!.createCart; + if (newCartId) { + localStorage.setItem(CART_ID, newCartId); + setCartId(newCartId); + } + } catch (err) { + console.error("Lỗi khi tạo giỏ hàng:", err); + } } - } catch { - localStorage.removeItem(STORAGE_KEY); + }; + + if (error) createCartFunc(); + else { + const localCartId = localStorage.getItem(CART_ID); + if (localCartId) setCartId(localCartId); } - }, []); + }, [eateryData, createCart, data, loading]); useEffect(() => { - localStorage.setItem(STORAGE_KEY, JSON.stringify(items)); - }, [items]); + if (data?.getCart) setCart(data.getCart); + }, [data]); - const addToCart = (product: MenuItemEntity) => { - setItems((prev) => { - const index = prev.findIndex((i) => i.productId === product.id); - if (index === -1) { - return [ - ...prev, - { - productId: product.id!, - name: product.name, - description: product.description, - priceAtTimeOfAdding: product.price, - quantity: 1, - }, - ]; - } + const addToCart = async (product: CartItemEntity) => { + if (!cartId) return; - const next = [...prev]; - next[index] = { ...next[index], quantity: next[index].quantity + 1 }; - return next; + const { data: result } = await addMenuItem({ + variables: { + cartId, + menuItemId: product.productId!, + quantity: product.quantity, + }, }); + + if (result) setCart(result.addItem); }; - const increaseQty = (id: string) => { - setItems((prev) => - prev.map((item) => - item.productId === id ? { ...item, quantity: item.quantity + 1 } : item, - ), - ); + const setQuantity = async (id: string, quantity: number) => { + if (!cartId) return; + + const { data: result } = await addMenuItem({ + variables: { + cartId, + menuItemId: id, + quantity, + }, + }); + + if (result) setCart(result.addItem); }; - const decreaseQty = (id: string) => { - setItems((prev) => - prev - .map((item) => - item.productId === id - ? { ...item, quantity: Math.max(0, item.quantity - 1) } - : item, - ) - .filter((item) => item.quantity > 0), - ); - }; + const removeFromCart = (id: string) => setQuantity(id, 0); - const removeFromCart = (id: string) => { - setItems((prev) => prev.filter((item) => item.productId !== id)); - }; + const increaseQty = (id: string) => + setQuantity(id, cart.items.find((i) => i.productId == id)!.quantity + 1); - const setQuantity = (id: string, quantity: number) => { - const safeQty = Number.isFinite(quantity) - ? Math.max(0, Math.floor(quantity)) - : 0; - if (safeQty === 0) { - removeFromCart(id); - return; - } - - setItems((prev) => - prev.map((item) => - item.productId === id ? { ...item, quantity: safeQty } : item, - ), - ); - }; + const decreaseQty = (id: string) => + setQuantity(id, cart.items.find((i) => i.productId == id)!.quantity - 1); const totalItems = useMemo( - () => items.reduce((sum, item) => sum + item.quantity, 0), - [items], - ); - - const totalPrice = useMemo( - () => - items.reduce( - (sum, item) => sum + item.priceAtTimeOfAdding * item.quantity, - 0, - ), - [items], + () => cart?.items.reduce((sum, item) => sum + item.quantity, 0), + [cart], ); const value = useMemo( () => ({ - items, + items: cart?.items, totalItems, - totalPrice, + totalPrice: cart?.totalAmount, addToCart, increaseQty, decreaseQty, removeFromCart, setQuantity, }), - [items, totalItems, totalPrice], + [cart?.items, cart?.totalAmount], ); return {children}; diff --git a/lib/constants.ts b/lib/constants.ts index 25bc49d..d12dd53 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -33,189 +33,6 @@ export const SOCIAL_LINKS: SocialLinks = { website: "/", }; -// ===== MOCK PRODUCTS ===== -// Placeholder data – replace with real API calls when backend is ready -export const MOCK_PRODUCTS: MenuItemEntity[] = [ - { - id: "1", - name: "Black Coffee", - price: 25000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Traditional Vietnamese black coffee, rich and bold, brewed by hand with a phin filter.", - available: true, - }, - { - id: "2", - name: "Milk Coffee", - price: 30000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Aromatic condensed milk coffee, rich and creamy — a perfect blend of strong coffee and sweet condensed milk.", - available: true, - }, - { - id: "3", - name: "White Coffee", - - price: 32000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Light and mild, with less coffee and more milk — perfect for those just starting to enjoy coffee.", - available: true, - }, - { - id: "4", - name: "Egg Coffee", - - price: 45000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "A Hanoi specialty — smooth, velvety egg cream layered over a bold coffee base.", - available: true, - }, - { - id: "5", - name: "Peach Orange Lemongrass Tea", - - price: 35000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Fragrant peach tea with fresh orange and lemongrass — refreshing and wonderfully cooling.", - available: true, - }, - { - id: "6", - name: "Matcha Green Tea", - - price: 40000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Pure Japanese matcha with a signature light bitterness — aromatic, refreshing, and nutritious.", - available: true, - }, - { - id: "7", - name: "Lychee Jasmine Tea", - - price: 38000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Sweet lychee tea delicately infused with jasmine blossom — a calming and soulful sip.", - available: true, - }, - { - id: "8", - name: "Yogurt with Tapioca Pearls", - - price: 38000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Smooth, creamy yogurt paired with chewy black tapioca pearls — a perfectly balanced sweet and tangy treat.", - available: true, - }, - { - id: "9", - name: "Strawberry Yogurt", - - price: 40000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Chilled yogurt with fresh sweet-tart strawberries, packed with vitamins and minerals.", - available: true, - }, - { - id: "10", - name: "Fresh Orange Juice", - - price: 35000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "100% freshly squeezed orange juice, rich in vitamin C and great for your health.", - available: true, - }, - { - id: "11", - name: "Watermelon Juice", - - price: 30000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Ice-cold watermelon juice — instantly refreshing on hot summer days.", - available: true, - }, - { - id: "12", - name: "Caramel Latte", - - price: 45000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Sweet and indulgent caramel latte with a velvety milk foam topping and a drizzle of caramel sauce.", - available: true, - }, - { - id: "13", - name: "Vanilla Latte", - - price: 45000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Smooth and gentle vanilla latte with a delicate natural vanilla fragrance.", - available: true, - }, - { - id: "14", - name: "Toasted Butter Bread", - - price: 20000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Crispy toasted bread spread with fragrant butter and strawberry jam — the perfect coffee companion.", - available: true, - }, - { - id: "15", - name: "Caramel Flan", - - price: 25000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Silky smooth flan with a golden caramel topping that melts in your mouth.", - available: true, - }, - { - id: "16", - name: "Black Tapioca Pearls", - - price: 10000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Chewy black tapioca pearls — add them to any drink for extra flavor and texture.", - available: true, - }, - { - id: "17", - name: "Coffee Jelly", - - price: 10000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Cool coffee jelly that adds a distinctive flavor boost to your drink.", - available: true, - }, - { - id: "18", - name: "White Tapioca Pearls", - - price: 10000, - imageUrl: "/imgs/products/placeholder.jpg", - description: - "Chewy white tapioca pearls — add them to any drink for extra flavor and texture.", - available: true, - }, -]; - // ===== MOCK SHOPS (for Feed page) ===== export const MOCK_SHOPS: Shop[] = [ { diff --git a/lib/types.ts b/lib/types.ts index a7121c7..1730227 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -125,6 +125,7 @@ export interface MenuItemEntity { export interface ShiftEntity {} export interface EateryEntity { + id: string; ownerId: string; name: string; menuItems: MenuItemEntity[]; @@ -150,10 +151,24 @@ export interface deleteMenuItemMutation { export interface CartItemEntity { productId: string; quantity: number; - priceAtTimeOfAdding: number; + priceAtTimeOfAdding?: number; } export interface CartEntity { Id: string; items: CartItemEntity[]; -} \ No newline at end of file + totalAmount: number; + paymentQrUrl: string; +} + +export interface getCartQuery { + getCart: CartEntity; +} + +export interface createCartMutation { + createCart: string; +} + +export interface addMenuItemMutation { + addItem: CartEntity; +}