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 */}
{
+ 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;
+}