diff --git a/.changeset/cart-components-mutations.md b/.changeset/cart-components-mutations.md new file mode 100644 index 00000000..74b6a5cf --- /dev/null +++ b/.changeset/cart-components-mutations.md @@ -0,0 +1,7 @@ +--- +"@godaddy/react": patch +--- + +Added cart functionality with `useAddToCart` hook and `Cart` component. +Fixed hydration mismatch in `Cart` component. +Removed internal environment references. diff --git a/examples/nextjs/app/providers.tsx b/examples/nextjs/app/providers.tsx index 71f3d85a..d96f8d77 100644 --- a/examples/nextjs/app/providers.tsx +++ b/examples/nextjs/app/providers.tsx @@ -25,6 +25,7 @@ export function Providers({ children }: { children: React.ReactNode }) { apiHost={process.env.NEXT_PUBLIC_GODADDY_API_HOST} storeId={process.env.NEXT_PUBLIC_GODADDY_STORE_ID} clientId={process.env.NEXT_PUBLIC_GODADDY_CLIENT_ID} + channelId={process.env.NEXT_PUBLIC_GODADDY_CHANNEL_ID} Link={Link} appearance={{ variables: { primary: '#ff0000', 'primary-foreground': '#FFFFFF' }, diff --git a/examples/nextjs/app/store/actions.ts b/examples/nextjs/app/store/actions.ts new file mode 100644 index 00000000..1cc06f39 --- /dev/null +++ b/examples/nextjs/app/store/actions.ts @@ -0,0 +1,87 @@ +'use server'; + +import { createCheckoutSession } from '@godaddy/react/server'; +import { redirect } from 'next/navigation'; + +export async function checkoutWithOrder(orderId: string) { + const session = await createCheckoutSession( + { + returnUrl: `https://godaddy.com`, + successUrl: `https://godaddy.com/success`, + draftOrderId: orderId, + storeId: process.env.NEXT_PUBLIC_GODADDY_STORE_ID || '', + channelId: process.env.NEXT_PUBLIC_GODADDY_CHANNEL_ID || '', + enableShippingAddressCollection: true, + enableBillingAddressCollection: true, + enableTaxCollection: true, + shipping: { + fulfillmentLocationId: 'default-location', + originAddress: { + addressLine1: '1600 Pennsylvania Ave NW', + adminArea1: 'DC', + adminArea3: 'Washington', + countryCode: 'US', + postalCode: '20500', + }, + }, + taxes: { + originAddress: { + addressLine1: '1600 Pennsylvania Ave NW', + adminArea1: 'DC', + adminArea3: 'Washington', + countryCode: 'US', + postalCode: '20500', + }, + }, + locations: [ + { + id: 'default-location', + isDefault: true, + address: { + addressLine1: '1600 Pennsylvania Ave NW', + adminArea1: 'DC', + adminArea3: 'Washington', + countryCode: 'US', + postalCode: '20500', + }, + }, + ], + paymentMethods: { + card: { + processor: 'godaddy', + checkoutTypes: ['standard'], + }, + express: { + processor: 'godaddy', + checkoutTypes: ['express'], + }, + paypal: { + processor: 'paypal', + checkoutTypes: ['standard'], + }, + offline: { + processor: 'offline', + checkoutTypes: ['standard'], + }, + }, + }, + { + auth: { + clientId: process.env.NEXT_PUBLIC_GODADDY_CLIENT_ID || '', + clientSecret: process.env.GODADDY_CLIENT_SECRET || '', + }, + } + ); + + if (!session) { + throw new Error('Failed to create checkout session'); + } + + console.log({ session }); + + if (!session.url) { + throw new Error('No checkout URL returned'); + } + + redirect(session.url); +} diff --git a/examples/nextjs/app/store/layout.tsx b/examples/nextjs/app/store/layout.tsx index 9b003c0e..a638c781 100644 --- a/examples/nextjs/app/store/layout.tsx +++ b/examples/nextjs/app/store/layout.tsx @@ -3,6 +3,7 @@ import { Cart } from '@godaddy/react'; import { ShoppingCart } from 'lucide-react'; import { createContext, useContext, useState } from 'react'; +import { checkoutWithOrder } from './actions'; interface CartContextType { openCart: () => void; @@ -25,10 +26,20 @@ export default function StoreLayout({ children: React.ReactNode; }) { const [isCartOpen, setIsCartOpen] = useState(false); + const [isCheckingOut, setIsCheckingOut] = useState(false); const openCart = () => setIsCartOpen(true); const closeCart = () => setIsCartOpen(false); + const handleCheckout = async (orderId: string) => { + setIsCheckingOut(true); + try { + await checkoutWithOrder(orderId); + } catch (_error) { + setIsCheckingOut(false); + } + }; + return (
@@ -43,7 +54,12 @@ export default function StoreLayout({ {children} - +
); diff --git a/examples/nextjs/app/store/product/[productId]/product.tsx b/examples/nextjs/app/store/product/[productId]/product.tsx index bce327b8..9aa51c71 100644 --- a/examples/nextjs/app/store/product/[productId]/product.tsx +++ b/examples/nextjs/app/store/product/[productId]/product.tsx @@ -3,8 +3,11 @@ import { ProductDetails } from '@godaddy/react'; import { ArrowLeft } from 'lucide-react'; import Link from 'next/link'; +import { useCart } from '../../layout'; export default function Product({ productId }: { productId: string }) { + const { openCart } = useCart(); + return (
Back to Store - +
); } diff --git a/examples/nextjs/app/store/products.tsx b/examples/nextjs/app/store/products.tsx index 324a4b06..48a01459 100644 --- a/examples/nextjs/app/store/products.tsx +++ b/examples/nextjs/app/store/products.tsx @@ -1,11 +1,17 @@ 'use client'; import { ProductGrid } from '@godaddy/react'; +import { useCart } from './layout'; export default function ProductsPage() { + const { openCart } = useCart(); + return (
- `/store/product/${sku}`} /> + `/store/product/${sku}`} + onAddToCartSuccess={openCart} + />
); } diff --git a/packages/localizations/src/deDe.ts b/packages/localizations/src/deDe.ts index 139c2acf..8f884e1c 100644 --- a/packages/localizations/src/deDe.ts +++ b/packages/localizations/src/deDe.ts @@ -348,4 +348,39 @@ export const deDe = { DEPENDENCY_ERROR: 'Wir können Ihre Bestellung derzeit nicht bearbeiten. Bitte warten Sie einen Moment und versuchen Sie es erneut', }, + storefront: { + product: 'Produkt', + sale: 'SALE', + noImage: 'Kein Bild', + noImageAvailable: 'Kein Bild verfügbar', + selectOptions: 'Optionen auswählen', + adding: 'Wird hinzugefügt...', + addToCart: 'In den Warenkorb', + shoppingCart: 'Warenkorb', + failedToLoadCart: 'Warenkorb konnte nicht geladen werden:', + retry: 'Wiederholen', + yourCartIsEmpty: 'Ihr Warenkorb ist leer', + addItemsToGetStarted: 'Fügen Sie Artikel hinzu, um zu beginnen', + errorLoadingProducts: 'Fehler beim Laden der Produkte:', + errorLoadingProduct: 'Fehler beim Laden des Produkts:', + productNotFound: 'Produkt nicht gefunden', + loadingVariantDetails: 'Variantendetails werden geladen...', + combinationNotAvailable: + 'Diese Kombination ist nicht verfügbar. Bitte wählen Sie andere Optionen.', + variantsMatch: + 'Varianten entsprechen Ihrer Auswahl. Wählen Sie weitere Attribute aus, um die Auswahl einzugrenzen.', + quantity: 'Menge', + addingToCart: 'Wird zum Warenkorb hinzugefügt...', + outOfStock: 'Nicht vorrätig', + viewDetails: 'Details anzeigen', + productType: 'Produkttyp:', + productId: 'Produkt-ID:', + selectedSku: 'Ausgewählte SKU:', + stockStatus: 'Lagerstatus:', + lowStock: 'Geringer Lagerbestand', + inStock: 'Auf Lager', + remove: 'Entfernen', + removing: 'Wird entfernt...', + checkout: 'Zur Kasse', + }, }; diff --git a/packages/localizations/src/enIe.ts b/packages/localizations/src/enIe.ts index 04f21481..ec6a20ae 100644 --- a/packages/localizations/src/enIe.ts +++ b/packages/localizations/src/enIe.ts @@ -325,4 +325,39 @@ export const enIe = { DEPENDENCY_ERROR: "We're unable to process your order right now. Please wait a moment and try again", }, + storefront: { + product: 'Product', + sale: 'SALE', + noImage: 'No image', + noImageAvailable: 'No image available', + selectOptions: 'Select Options', + adding: 'Adding...', + addToCart: 'Add to Cart', + shoppingCart: 'Shopping Cart', + failedToLoadCart: 'Failed to load cart:', + retry: 'Retry', + yourCartIsEmpty: 'Your cart is empty', + addItemsToGetStarted: 'Add items to get started', + errorLoadingProducts: 'Error loading products:', + errorLoadingProduct: 'Error loading product:', + productNotFound: 'Product not found', + loadingVariantDetails: 'Loading variant details...', + combinationNotAvailable: + 'This combination is not available. Please select different options.', + variantsMatch: + 'variants match your selection. Select more attributes to narrow down.', + quantity: 'Quantity', + addingToCart: 'Adding to Cart...', + outOfStock: 'Out of Stock', + viewDetails: 'View Details', + productType: 'Product Type:', + productId: 'Product ID:', + selectedSku: 'Selected SKU:', + stockStatus: 'Stock Status:', + lowStock: 'Low Stock', + inStock: 'In Stock', + remove: 'Remove', + removing: 'Removing...', + checkout: 'Checkout', + }, }; diff --git a/packages/localizations/src/enUs.ts b/packages/localizations/src/enUs.ts index 89d960a6..80fc5793 100644 --- a/packages/localizations/src/enUs.ts +++ b/packages/localizations/src/enUs.ts @@ -325,4 +325,39 @@ export const enUs = { DEPENDENCY_ERROR: "We're unable to process your order right now. Please wait a moment and try again", }, + storefront: { + product: 'Product', + sale: 'SALE', + noImage: 'No image', + noImageAvailable: 'No image available', + selectOptions: 'Select Options', + adding: 'Adding...', + addToCart: 'Add to Cart', + shoppingCart: 'Shopping Cart', + failedToLoadCart: 'Failed to load cart:', + retry: 'Retry', + yourCartIsEmpty: 'Your cart is empty', + addItemsToGetStarted: 'Add items to get started', + errorLoadingProducts: 'Error loading products:', + errorLoadingProduct: 'Error loading product:', + productNotFound: 'Product not found', + loadingVariantDetails: 'Loading variant details...', + combinationNotAvailable: + 'This combination is not available. Please select different options.', + variantsMatch: + 'variants match your selection. Select more attributes to narrow down.', + quantity: 'Quantity', + addingToCart: 'Adding to Cart...', + outOfStock: 'Out of Stock', + viewDetails: 'View Details', + productType: 'Product Type:', + productId: 'Product ID:', + selectedSku: 'Selected SKU:', + stockStatus: 'Stock Status:', + lowStock: 'Low Stock', + inStock: 'In Stock', + remove: 'Remove', + removing: 'Removing...', + checkout: 'Checkout', + }, }; diff --git a/packages/localizations/src/esAr.ts b/packages/localizations/src/esAr.ts index e6f6a692..eb0df26c 100644 --- a/packages/localizations/src/esAr.ts +++ b/packages/localizations/src/esAr.ts @@ -331,4 +331,39 @@ export const esAr = { DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', }, + storefront: { + product: 'Producto', + sale: 'OFERTA', + noImage: 'Sin imagen', + noImageAvailable: 'Sin imagen disponible', + selectOptions: 'Seleccionar opciones', + adding: 'Agregando...', + addToCart: 'Agregar al carrito', + shoppingCart: 'Carrito de compras', + failedToLoadCart: 'Error al cargar el carrito:', + retry: 'Reintentar', + yourCartIsEmpty: 'Tu carrito está vacío', + addItemsToGetStarted: 'Agregá artículos para comenzar', + errorLoadingProducts: 'Error al cargar productos:', + errorLoadingProduct: 'Error al cargar producto:', + productNotFound: 'Producto no encontrado', + loadingVariantDetails: 'Cargando detalles de la variante...', + combinationNotAvailable: + 'Esta combinación no está disponible. Por favor seleccioná opciones diferentes.', + variantsMatch: + 'variantes coinciden con tu selección. Seleccioná más atributos para reducir las opciones.', + quantity: 'Cantidad', + addingToCart: 'Agregando al carrito...', + outOfStock: 'Agotado', + viewDetails: 'Ver detalles', + productType: 'Tipo de producto:', + productId: 'ID del producto:', + selectedSku: 'SKU seleccionado:', + stockStatus: 'Estado del stock:', + lowStock: 'Stock bajo', + inStock: 'En stock', + remove: 'Eliminar', + removing: 'Eliminando...', + checkout: 'Pagar', + }, }; diff --git a/packages/localizations/src/esCl.ts b/packages/localizations/src/esCl.ts index ba005740..c09cf98f 100644 --- a/packages/localizations/src/esCl.ts +++ b/packages/localizations/src/esCl.ts @@ -333,4 +333,39 @@ export const esCl = { DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', }, + storefront: { + product: 'Producto', + sale: 'OFERTA', + noImage: 'Sin imagen', + noImageAvailable: 'Sin imagen disponible', + selectOptions: 'Seleccionar opciones', + adding: 'Agregando...', + addToCart: 'Agregar al carrito', + shoppingCart: 'Carrito de compras', + failedToLoadCart: 'Error al cargar el carrito:', + retry: 'Reintentar', + yourCartIsEmpty: 'Tu carrito está vacío', + addItemsToGetStarted: 'Agrega artículos para comenzar', + errorLoadingProducts: 'Error al cargar productos:', + errorLoadingProduct: 'Error al cargar producto:', + productNotFound: 'Producto no encontrado', + loadingVariantDetails: 'Cargando detalles de la variante...', + combinationNotAvailable: + 'Esta combinación no está disponible. Por favor selecciona opciones diferentes.', + variantsMatch: + 'variantes coinciden con tu selección. Selecciona más atributos para reducir las opciones.', + quantity: 'Cantidad', + addingToCart: 'Agregando al carrito...', + outOfStock: 'Agotado', + viewDetails: 'Ver detalles', + productType: 'Tipo de producto:', + productId: 'ID del producto:', + selectedSku: 'SKU seleccionado:', + stockStatus: 'Estado del stock:', + lowStock: 'Stock bajo', + inStock: 'En stock', + remove: 'Eliminar', + removing: 'Eliminando...', + checkout: 'Pagar', + }, }; diff --git a/packages/localizations/src/esCo.ts b/packages/localizations/src/esCo.ts index 0833baae..5ee93d15 100644 --- a/packages/localizations/src/esCo.ts +++ b/packages/localizations/src/esCo.ts @@ -331,4 +331,39 @@ export const esCo = { DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', }, + storefront: { + product: 'Producto', + sale: 'OFERTA', + noImage: 'Sin imagen', + noImageAvailable: 'Sin imagen disponible', + selectOptions: 'Seleccionar opciones', + adding: 'Agregando...', + addToCart: 'Agregar al carrito', + shoppingCart: 'Carrito de compras', + failedToLoadCart: 'Error al cargar el carrito:', + retry: 'Reintentar', + yourCartIsEmpty: 'Tu carrito está vacío', + addItemsToGetStarted: 'Agrega artículos para comenzar', + errorLoadingProducts: 'Error al cargar productos:', + errorLoadingProduct: 'Error al cargar producto:', + productNotFound: 'Producto no encontrado', + loadingVariantDetails: 'Cargando detalles de la variante...', + combinationNotAvailable: + 'Esta combinación no está disponible. Por favor selecciona opciones diferentes.', + variantsMatch: + 'variantes coinciden con tu selección. Selecciona más atributos para reducir las opciones.', + quantity: 'Cantidad', + addingToCart: 'Agregando al carrito...', + outOfStock: 'Agotado', + viewDetails: 'Ver detalles', + productType: 'Tipo de producto:', + productId: 'ID del producto:', + selectedSku: 'SKU seleccionado:', + stockStatus: 'Estado del stock:', + lowStock: 'Stock bajo', + inStock: 'En stock', + remove: 'Eliminar', + removing: 'Eliminando...', + checkout: 'Pagar', + }, }; diff --git a/packages/localizations/src/esEs.ts b/packages/localizations/src/esEs.ts index 349a43fe..5ce4fd31 100644 --- a/packages/localizations/src/esEs.ts +++ b/packages/localizations/src/esEs.ts @@ -336,4 +336,39 @@ export const esEs = { DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', }, + storefront: { + product: 'Producto', + sale: 'OFERTA', + noImage: 'Sin imagen', + noImageAvailable: 'Sin imagen disponible', + selectOptions: 'Seleccionar opciones', + adding: 'Añadiendo...', + addToCart: 'Añadir al carrito', + shoppingCart: 'Carrito de compras', + failedToLoadCart: 'Error al cargar el carrito:', + retry: 'Reintentar', + yourCartIsEmpty: 'Tu carrito está vacío', + addItemsToGetStarted: 'Añade artículos para comenzar', + errorLoadingProducts: 'Error al cargar productos:', + errorLoadingProduct: 'Error al cargar producto:', + productNotFound: 'Producto no encontrado', + loadingVariantDetails: 'Cargando detalles de la variante...', + combinationNotAvailable: + 'Esta combinación no está disponible. Por favor seleccione opciones diferentes.', + variantsMatch: + 'variantes coinciden con tu selección. Selecciona más atributos para reducir las opciones.', + quantity: 'Cantidad', + addingToCart: 'Añadiendo al carrito...', + outOfStock: 'Agotado', + viewDetails: 'Ver detalles', + productType: 'Tipo de producto:', + productId: 'ID del producto:', + selectedSku: 'SKU seleccionado:', + stockStatus: 'Estado del stock:', + lowStock: 'Stock bajo', + inStock: 'En stock', + remove: 'Eliminar', + removing: 'Eliminando...', + checkout: 'Pagar', + }, }; diff --git a/packages/localizations/src/esMx.ts b/packages/localizations/src/esMx.ts index e7bd89a1..cefaa7b1 100644 --- a/packages/localizations/src/esMx.ts +++ b/packages/localizations/src/esMx.ts @@ -332,4 +332,39 @@ export const esMx = { DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', }, + storefront: { + product: 'Producto', + sale: 'OFERTA', + noImage: 'Sin imagen', + noImageAvailable: 'Sin imagen disponible', + selectOptions: 'Seleccionar opciones', + adding: 'Agregando...', + addToCart: 'Agregar al carrito', + shoppingCart: 'Carrito de compras', + failedToLoadCart: 'Error al cargar el carrito:', + retry: 'Reintentar', + yourCartIsEmpty: 'Tu carrito está vacío', + addItemsToGetStarted: 'Agrega artículos para comenzar', + errorLoadingProducts: 'Error al cargar productos:', + errorLoadingProduct: 'Error al cargar producto:', + productNotFound: 'Producto no encontrado', + loadingVariantDetails: 'Cargando detalles de la variante...', + combinationNotAvailable: + 'Esta combinación no está disponible. Por favor selecciona opciones diferentes.', + variantsMatch: + 'variantes coinciden con tu selección. Selecciona más atributos para reducir las opciones.', + quantity: 'Cantidad', + addingToCart: 'Agregando al carrito...', + outOfStock: 'Agotado', + viewDetails: 'Ver detalles', + productType: 'Tipo de producto:', + productId: 'ID del producto:', + selectedSku: 'SKU seleccionado:', + stockStatus: 'Estado del stock:', + lowStock: 'Stock bajo', + inStock: 'En stock', + remove: 'Eliminar', + removing: 'Eliminando...', + checkout: 'Pagar', + }, }; diff --git a/packages/localizations/src/esPe.ts b/packages/localizations/src/esPe.ts index ef3876e2..b2c01a8d 100644 --- a/packages/localizations/src/esPe.ts +++ b/packages/localizations/src/esPe.ts @@ -331,4 +331,39 @@ export const esPe = { DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', }, + storefront: { + product: 'Producto', + sale: 'OFERTA', + noImage: 'Sin imagen', + noImageAvailable: 'Sin imagen disponible', + selectOptions: 'Seleccionar opciones', + adding: 'Agregando...', + addToCart: 'Agregar al carrito', + shoppingCart: 'Carrito de compras', + failedToLoadCart: 'Error al cargar el carrito:', + retry: 'Reintentar', + yourCartIsEmpty: 'Tu carrito está vacío', + addItemsToGetStarted: 'Agrega artículos para comenzar', + errorLoadingProducts: 'Error al cargar productos:', + errorLoadingProduct: 'Error al cargar producto:', + productNotFound: 'Producto no encontrado', + loadingVariantDetails: 'Cargando detalles de la variante...', + combinationNotAvailable: + 'Esta combinación no está disponible. Por favor selecciona opciones diferentes.', + variantsMatch: + 'variantes coinciden con tu selección. Selecciona más atributos para reducir las opciones.', + quantity: 'Cantidad', + addingToCart: 'Agregando al carrito...', + outOfStock: 'Agotado', + viewDetails: 'Ver detalles', + productType: 'Tipo de producto:', + productId: 'ID del producto:', + selectedSku: 'SKU seleccionado:', + stockStatus: 'Estado del stock:', + lowStock: 'Stock bajo', + inStock: 'En stock', + remove: 'Eliminar', + removing: 'Eliminando...', + checkout: 'Pagar', + }, }; diff --git a/packages/localizations/src/esUs.ts b/packages/localizations/src/esUs.ts index 89bbf0fb..55fd477a 100644 --- a/packages/localizations/src/esUs.ts +++ b/packages/localizations/src/esUs.ts @@ -331,4 +331,39 @@ export const esUs = { DEPENDENCY_ERROR: 'No podemos procesar su pedido en este momento. Espere un momento e inténtelo de nuevo', }, + storefront: { + product: 'Producto', + sale: 'OFERTA', + noImage: 'Sin imagen', + noImageAvailable: 'Sin imagen disponible', + selectOptions: 'Seleccionar opciones', + adding: 'Agregando...', + addToCart: 'Agregar al carrito', + shoppingCart: 'Carrito de compras', + failedToLoadCart: 'Error al cargar el carrito:', + retry: 'Reintentar', + yourCartIsEmpty: 'Tu carrito está vacío', + addItemsToGetStarted: 'Agrega artículos para comenzar', + errorLoadingProducts: 'Error al cargar productos:', + errorLoadingProduct: 'Error al cargar producto:', + productNotFound: 'Producto no encontrado', + loadingVariantDetails: 'Cargando detalles de la variante...', + combinationNotAvailable: + 'Esta combinación no está disponible. Por favor selecciona opciones diferentes.', + variantsMatch: + 'variantes coinciden con tu selección. Selecciona más atributos para reducir las opciones.', + quantity: 'Cantidad', + addingToCart: 'Agregando al carrito...', + outOfStock: 'Agotado', + viewDetails: 'Ver detalles', + productType: 'Tipo de producto:', + productId: 'ID del producto:', + selectedSku: 'SKU seleccionado:', + stockStatus: 'Estado del stock:', + lowStock: 'Stock bajo', + inStock: 'En stock', + remove: 'Eliminar', + removing: 'Eliminando...', + checkout: 'Pagar', + }, }; diff --git a/packages/localizations/src/frCa.ts b/packages/localizations/src/frCa.ts index ae5a2310..e798ec28 100644 --- a/packages/localizations/src/frCa.ts +++ b/packages/localizations/src/frCa.ts @@ -348,4 +348,39 @@ export const frCa = { DEPENDENCY_ERROR: 'Nous ne pouvons pas traiter votre commande actuellement. Veuillez patienter un moment et réessayer', }, + storefront: { + product: 'Produit', + sale: 'SOLDE', + noImage: "Pas d'image", + noImageAvailable: 'Aucune image disponible', + selectOptions: 'Sélectionner les options', + adding: 'Ajout en cours...', + addToCart: 'Ajouter au panier', + shoppingCart: 'Panier', + failedToLoadCart: 'Échec du chargement du panier:', + retry: 'Réessayer', + yourCartIsEmpty: 'Votre panier est vide', + addItemsToGetStarted: 'Ajoutez des articles pour commencer', + errorLoadingProducts: 'Erreur lors du chargement des produits:', + errorLoadingProduct: 'Erreur lors du chargement du produit:', + productNotFound: 'Produit non trouvé', + loadingVariantDetails: 'Chargement des détails de la variante...', + combinationNotAvailable: + "Cette combinaison n'est pas disponible. Veuillez sélectionner d'autres options.", + variantsMatch: + "variantes correspondent à votre sélection. Sélectionnez plus d'attributs pour affiner.", + quantity: 'Quantité', + addingToCart: 'Ajout au panier...', + outOfStock: 'Rupture de stock', + viewDetails: 'Voir les détails', + productType: 'Type de produit:', + productId: 'ID du produit:', + selectedSku: 'SKU sélectionné:', + stockStatus: 'État du stock:', + lowStock: 'Stock faible', + inStock: 'En stock', + remove: 'Supprimer', + removing: 'Suppression...', + checkout: 'Commander', + }, }; diff --git a/packages/localizations/src/frFr.ts b/packages/localizations/src/frFr.ts index d4ba9fc9..312851b6 100644 --- a/packages/localizations/src/frFr.ts +++ b/packages/localizations/src/frFr.ts @@ -349,4 +349,39 @@ export const frFr = { DEPENDENCY_ERROR: 'Nous ne pouvons pas traiter votre commande actuellement. Veuillez patienter un moment et réessayer', }, + storefront: { + product: 'Produit', + sale: 'SOLDE', + noImage: "Pas d'image", + noImageAvailable: 'Aucune image disponible', + selectOptions: 'Sélectionner les options', + adding: 'Ajout en cours...', + addToCart: 'Ajouter au panier', + shoppingCart: 'Panier', + failedToLoadCart: 'Échec du chargement du panier:', + retry: 'Réessayer', + yourCartIsEmpty: 'Votre panier est vide', + addItemsToGetStarted: 'Ajoutez des articles pour commencer', + errorLoadingProducts: 'Erreur lors du chargement des produits:', + errorLoadingProduct: 'Erreur lors du chargement du produit:', + productNotFound: 'Produit non trouvé', + loadingVariantDetails: 'Chargement des détails de la variante...', + combinationNotAvailable: + "Cette combinaison n'est pas disponible. Veuillez sélectionner d'autres options.", + variantsMatch: + "variantes correspondent à votre sélection. Sélectionnez plus d'attributs pour affiner.", + quantity: 'Quantité', + addingToCart: 'Ajout au panier...', + outOfStock: 'Rupture de stock', + viewDetails: 'Voir les détails', + productType: 'Type de produit:', + productId: 'ID du produit:', + selectedSku: 'SKU sélectionné:', + stockStatus: 'État du stock:', + lowStock: 'Stock faible', + inStock: 'En stock', + remove: 'Supprimer', + removing: 'Suppression...', + checkout: 'Commander', + }, }; diff --git a/packages/localizations/src/idId.ts b/packages/localizations/src/idId.ts index 29d9aff7..f5206314 100644 --- a/packages/localizations/src/idId.ts +++ b/packages/localizations/src/idId.ts @@ -324,4 +324,39 @@ export const idId = { DEPENDENCY_ERROR: 'Kami tidak dapat memproses pesanan Anda saat ini. Silakan tunggu sebentar dan coba lagi', }, + storefront: { + product: 'Produk', + sale: 'OBRAL', + noImage: 'Tidak ada gambar', + noImageAvailable: 'Tidak ada gambar tersedia', + selectOptions: 'Pilih opsi', + adding: 'Menambahkan...', + addToCart: 'Tambahkan ke keranjang', + shoppingCart: 'Keranjang belanja', + failedToLoadCart: 'Gagal memuat keranjang:', + retry: 'Coba lagi', + yourCartIsEmpty: 'Keranjang Anda kosong', + addItemsToGetStarted: 'Tambahkan item untuk memulai', + errorLoadingProducts: 'Error memuat produk:', + errorLoadingProduct: 'Error memuat produk:', + productNotFound: 'Produk tidak ditemukan', + loadingVariantDetails: 'Memuat detail varian...', + combinationNotAvailable: + 'Kombinasi ini tidak tersedia. Silakan pilih opsi yang berbeda.', + variantsMatch: + 'varian cocok dengan pilihan Anda. Pilih lebih banyak atribut untuk mempersempit.', + quantity: 'Jumlah', + addingToCart: 'Menambahkan ke keranjang...', + outOfStock: 'Stok habis', + viewDetails: 'Lihat Detail', + productType: 'Jenis Produk:', + productId: 'ID Produk:', + selectedSku: 'SKU yang dipilih:', + stockStatus: 'Status Stok:', + lowStock: 'Stok rendah', + inStock: 'Tersedia', + remove: 'Hapus', + removing: 'Menghapus...', + checkout: 'Checkout', + }, }; diff --git a/packages/localizations/src/itIt.ts b/packages/localizations/src/itIt.ts index a26a7298..20bba839 100644 --- a/packages/localizations/src/itIt.ts +++ b/packages/localizations/src/itIt.ts @@ -348,4 +348,39 @@ export const itIt = { DEPENDENCY_ERROR: 'Non riusciamo a elaborare il tuo ordine in questo momento. Aspetta un momento e riprova', }, + storefront: { + product: 'Prodotto', + sale: 'SALDO', + noImage: 'Nessuna immagine', + noImageAvailable: 'Nessuna immagine disponibile', + selectOptions: 'Seleziona opzioni', + adding: 'Aggiunta in corso...', + addToCart: 'Aggiungi al carrello', + shoppingCart: 'Carrello', + failedToLoadCart: 'Caricamento carrello non riuscito:', + retry: 'Riprova', + yourCartIsEmpty: 'Il tuo carrello è vuoto', + addItemsToGetStarted: 'Aggiungi articoli per iniziare', + errorLoadingProducts: 'Errore nel caricamento dei prodotti:', + errorLoadingProduct: 'Errore nel caricamento del prodotto:', + productNotFound: 'Prodotto non trovato', + loadingVariantDetails: 'Caricamento dei dettagli della variante...', + combinationNotAvailable: + 'Questa combinazione non è disponibile. Si prega di selezionare opzioni diverse.', + variantsMatch: + 'varianti corrispondono alla tua selezione. Seleziona più attributi per restringere.', + quantity: 'Quantità', + addingToCart: 'Aggiunta al carrello...', + outOfStock: 'Esaurito', + viewDetails: 'Vedi dettagli', + productType: 'Tipo di prodotto:', + productId: 'ID prodotto:', + selectedSku: 'SKU selezionato:', + stockStatus: 'Stato delle scorte:', + lowStock: 'Scorte limitate', + inStock: 'Disponibile', + remove: 'Rimuovi', + removing: 'Rimozione...', + checkout: 'Acquista', + }, }; diff --git a/packages/localizations/src/ptBr.ts b/packages/localizations/src/ptBr.ts index 78fa8072..79cd3b0d 100644 --- a/packages/localizations/src/ptBr.ts +++ b/packages/localizations/src/ptBr.ts @@ -329,4 +329,39 @@ export const ptBr = { DEPENDENCY_ERROR: 'Não conseguimos processar seu pedido no momento. Aguarde um momento e tente novamente', }, + storefront: { + product: 'Produto', + sale: 'PROMOÇÃO', + noImage: 'Sem imagem', + noImageAvailable: 'Nenhuma imagem disponível', + selectOptions: 'Selecionar opções', + adding: 'Adicionando...', + addToCart: 'Adicionar ao carrinho', + shoppingCart: 'Carrinho de compras', + failedToLoadCart: 'Falha ao carregar carrinho:', + retry: 'Tentar novamente', + yourCartIsEmpty: 'Seu carrinho está vazio', + addItemsToGetStarted: 'Adicione itens para começar', + errorLoadingProducts: 'Erro ao carregar produtos:', + errorLoadingProduct: 'Erro ao carregar produto:', + productNotFound: 'Produto não encontrado', + loadingVariantDetails: 'Carregando detalhes da variante...', + combinationNotAvailable: + 'Esta combinação não está disponível. Por favor, selecione opções diferentes.', + variantsMatch: + 'variantes correspondem à sua seleção. Selecione mais atributos para refinar.', + quantity: 'Quantidade', + addingToCart: 'Adicionando ao carrinho...', + outOfStock: 'Fora de estoque', + viewDetails: 'Ver detalhes', + productType: 'Tipo de produto:', + productId: 'ID do produto:', + selectedSku: 'SKU selecionado:', + stockStatus: 'Status do estoque:', + lowStock: 'Estoque baixo', + inStock: 'Em estoque', + remove: 'Remover', + removing: 'Removendo...', + checkout: 'Finalizar compra', + }, }; diff --git a/packages/localizations/src/qaPs.ts b/packages/localizations/src/qaPs.ts index c0e35fa8..0edfdcab 100644 --- a/packages/localizations/src/qaPs.ts +++ b/packages/localizations/src/qaPs.ts @@ -333,4 +333,39 @@ export const qaPs = { DEPENDENCY_ERROR: 'موږ اوس ستاسو امر پروسس نشو کولی. مهرباني وکړئ یو شېبه انتظار وکړئ او بیا هڅه وکړئ', }, + storefront: { + product: '[Product]', + sale: '[SALE]', + noImage: '[No image]', + noImageAvailable: '[No image available]', + selectOptions: '[Select Options]', + adding: '[Adding...]', + addToCart: '[Add to Cart]', + shoppingCart: '[Shopping Cart]', + failedToLoadCart: '[Failed to load cart:]', + retry: '[Retry]', + yourCartIsEmpty: '[Your cart is empty]', + addItemsToGetStarted: '[Add items to get started]', + errorLoadingProducts: '[Error loading products:]', + errorLoadingProduct: '[Error loading product:]', + productNotFound: '[Product not found]', + loadingVariantDetails: '[Loading variant details...]', + combinationNotAvailable: + '[This combination is not available. Please select different options.]', + variantsMatch: + '[variants match your selection. Select more attributes to narrow down.]', + quantity: '[Quantity]', + addingToCart: '[Adding to Cart...]', + outOfStock: '[Out of Stock]', + viewDetails: '[View Details]', + productType: '[Product Type:]', + productId: '[Product ID:]', + selectedSku: '[Selected SKU:]', + stockStatus: '[Stock Status:]', + lowStock: '[Low Stock]', + inStock: '[In Stock]', + remove: '[Remove]', + removing: '[Removing...]', + checkout: '[Checkout]', + }, }; diff --git a/packages/localizations/src/trTr.ts b/packages/localizations/src/trTr.ts index a2d1a5bd..ae4cb1d1 100644 --- a/packages/localizations/src/trTr.ts +++ b/packages/localizations/src/trTr.ts @@ -324,4 +324,39 @@ export const trTr = { DEPENDENCY_ERROR: 'Şu anda siparişinizi işleme alamıyoruz. Lütfen bir dakika bekleyin ve tekrar deneyin', }, + storefront: { + product: 'Ürün', + sale: 'İNDİRİM', + noImage: 'Resim yok', + noImageAvailable: 'Resim mevcut değil', + selectOptions: 'Seçenekleri seçin', + adding: 'Ekleniyor...', + addToCart: 'Sepete ekle', + shoppingCart: 'Alışveriş sepeti', + failedToLoadCart: 'Sepet yüklenemedi:', + retry: 'Yeniden dene', + yourCartIsEmpty: 'Sepetiniz boş', + addItemsToGetStarted: 'Başlamak için ürün ekleyin', + errorLoadingProducts: 'Ürünler yüklenirken hata:', + errorLoadingProduct: 'Ürün yüklenirken hata:', + productNotFound: 'Ürün bulunamadı', + loadingVariantDetails: 'Varyant detayları yükleniyor...', + combinationNotAvailable: + 'Bu kombinasyon mevcut değil. Lütfen farklı seçenekler seçin.', + variantsMatch: + 'varyant seçiminizle eşleşiyor. Daraltmak için daha fazla özellik seçin.', + quantity: 'Miktar', + addingToCart: 'Sepete ekleniyor...', + outOfStock: 'Stokta yok', + viewDetails: 'Detayları Görüntüle', + productType: 'Ürün Tipi:', + productId: 'Ürün ID:', + selectedSku: 'Seçili SKU:', + stockStatus: 'Stok Durumu:', + lowStock: 'Düşük stok', + inStock: 'Stokta', + remove: 'Kaldır', + removing: 'Kaldırılıyor...', + checkout: 'Ödeme yap', + }, }; diff --git a/packages/localizations/src/viVn.ts b/packages/localizations/src/viVn.ts index b5d031f1..e4660311 100644 --- a/packages/localizations/src/viVn.ts +++ b/packages/localizations/src/viVn.ts @@ -325,4 +325,39 @@ export const viVn = { DEPENDENCY_ERROR: 'Chúng tôi không thể xử lý đơn hàng của bạn ngay bây giờ. Vui lòng đợi một chút và thử lại', }, + storefront: { + product: 'Sản phẩm', + sale: 'GIẢM GIÁ', + noImage: 'Không có hình ảnh', + noImageAvailable: 'Không có hình ảnh', + selectOptions: 'Chọn tùy chọn', + adding: 'Đang thêm...', + addToCart: 'Thêm vào giỏ hàng', + shoppingCart: 'Giỏ hàng', + failedToLoadCart: 'Không tải được giỏ hàng:', + retry: 'Thử lại', + yourCartIsEmpty: 'Giỏ hàng của bạn trống', + addItemsToGetStarted: 'Thêm sản phẩm để bắt đầu', + errorLoadingProducts: 'Lỗi tải sản phẩm:', + errorLoadingProduct: 'Lỗi tải sản phẩm:', + productNotFound: 'Không tìm thấy sản phẩm', + loadingVariantDetails: 'Đang tải chi tiết biến thể...', + combinationNotAvailable: + 'Sự kết hợp này không có sẵn. Vui lòng chọn tùy chọn khác.', + variantsMatch: + 'biến thể phù hợp với lựa chọn của bạn. Chọn thêm thuộc tính để thu hẹp.', + quantity: 'Số lượng', + addingToCart: 'Đang thêm vào giỏ hàng...', + outOfStock: 'Hết hàng', + viewDetails: 'Xem chi tiết', + productType: 'Loại sản phẩm:', + productId: 'ID sản phẩm:', + selectedSku: 'SKU đã chọn:', + stockStatus: 'Trạng thái kho:', + lowStock: 'Sắp hết hàng', + inStock: 'Còn hàng', + remove: 'Xóa', + removing: 'Đang xóa...', + checkout: 'Thanh toán', + }, }; diff --git a/packages/localizations/src/zhCn.ts b/packages/localizations/src/zhCn.ts index 7e474e02..595fe1ed 100644 --- a/packages/localizations/src/zhCn.ts +++ b/packages/localizations/src/zhCn.ts @@ -314,4 +314,37 @@ export const zhCn = { MISSING_SHIPPING_INFO: '配送地址或方式应用失败', DEPENDENCY_ERROR: '我们目前无法处理您的订单。请稍等片刻再试', }, + storefront: { + product: '产品', + sale: '促销', + noImage: '无图片', + noImageAvailable: '无可用图片', + selectOptions: '选择选项', + adding: '正在添加...', + addToCart: '添加到购物车', + shoppingCart: '购物车', + failedToLoadCart: '加载购物车失败:', + retry: '重试', + yourCartIsEmpty: '您的购物车是空的', + addItemsToGetStarted: '添加商品以开始', + errorLoadingProducts: '加载产品时出错:', + errorLoadingProduct: '加载产品时出错:', + productNotFound: '未找到产品', + loadingVariantDetails: '正在加载变体详情...', + combinationNotAvailable: '此组合不可用。请选择不同的选项。', + variantsMatch: '个变体与您的选择匹配。选择更多属性以缩小范围。', + quantity: '数量', + addingToCart: '正在添加到购物车...', + outOfStock: '缺货', + viewDetails: '查看详情', + productType: '产品类型:', + productId: '产品ID:', + selectedSku: '已选SKU:', + stockStatus: '库存状态:', + lowStock: '库存不足', + inStock: '有货', + remove: '删除', + removing: '正在删除...', + checkout: '结账', + }, }; diff --git a/packages/localizations/src/zhSg.ts b/packages/localizations/src/zhSg.ts index 6bd0ea7f..915dc466 100644 --- a/packages/localizations/src/zhSg.ts +++ b/packages/localizations/src/zhSg.ts @@ -314,4 +314,37 @@ export const zhSg = { MISSING_SHIPPING_INFO: '配送地址或方式应用失败', DEPENDENCY_ERROR: '我們目前無法處理您的訂單。請稍等片刻再試', }, + storefront: { + product: '产品', + sale: '促销', + noImage: '无图片', + noImageAvailable: '无可用图片', + selectOptions: '选择选项', + adding: '正在添加...', + addToCart: '添加到购物车', + shoppingCart: '购物车', + failedToLoadCart: '加载购物车失败:', + retry: '重试', + yourCartIsEmpty: '您的购物车是空的', + addItemsToGetStarted: '添加商品以开始', + errorLoadingProducts: '加载产品时出错:', + errorLoadingProduct: '加载产品时出错:', + productNotFound: '未找到产品', + loadingVariantDetails: '正在加载变体详情...', + combinationNotAvailable: '此组合不可用。请选择不同的选项。', + variantsMatch: '个变体与您的选择匹配。选择更多属性以缩小范围。', + quantity: '数量', + addingToCart: '正在添加到购物车...', + outOfStock: '缺货', + viewDetails: '查看详情', + productType: '产品类型:', + productId: '产品ID:', + selectedSku: '已选SKU:', + stockStatus: '库存状态:', + lowStock: '库存不足', + inStock: '有货', + remove: '删除', + removing: '正在删除...', + checkout: '结账', + }, }; diff --git a/packages/react/src/components/checkout/line-items/line-items.tsx b/packages/react/src/components/checkout/line-items/line-items.tsx index 45a92786..196e9315 100644 --- a/packages/react/src/components/checkout/line-items/line-items.tsx +++ b/packages/react/src/components/checkout/line-items/line-items.tsx @@ -2,6 +2,7 @@ import { Image } from 'lucide-react'; import { useFormatCurrency } from '@/components/checkout/utils/format-currency'; +import { Button } from '@/components/ui/button.tsx'; import { useGoDaddyContext } from '@/godaddy-provider'; import type { SKUProduct } from '@/types'; @@ -57,12 +58,18 @@ export interface DraftOrderLineItemsProps { items: Product[]; currencyCode?: string; inputInMinorUnits?: boolean; + onRemoveFromCart?: (itemId: string) => void; + isRemovingFromCart?: boolean; + removingItemId?: string; } export function DraftOrderLineItems({ items, currencyCode = 'USD', inputInMinorUnits = false, + onRemoveFromCart, + isRemovingFromCart = false, + removingItemId, }: DraftOrderLineItemsProps) { const { t } = useGoDaddyContext(); const formatCurrency = useFormatCurrency(); @@ -126,12 +133,29 @@ export function DraftOrderLineItems({ ) : null} - {t.general.quantity}: {item.quantity} + {t.general.quantity}: {item.quantity} ·{' '} + {onRemoveFromCart ? ( + + ) : null} {item.originalPrice && item.quantity ? ( -
-
+
+
{formatCurrency({ amount: item.originalPrice * item.quantity, diff --git a/packages/react/src/components/checkout/utils/checkout-transformers.ts b/packages/react/src/components/checkout/utils/checkout-transformers.ts index 5382367c..d7d36314 100644 --- a/packages/react/src/components/checkout/utils/checkout-transformers.ts +++ b/packages/react/src/components/checkout/utils/checkout-transformers.ts @@ -157,7 +157,7 @@ export function mapSkusToItemsDisplay( return { id: orderItem.id || '', name: skuDetails?.label || orderItem.name || '', - image: orderItem.details?.productAssetUrl || skuDetails?.mediaUrls?.[0], + image: orderItem.details?.productAssetUrl, quantity: orderItem.quantity || 0, originalPrice: (orderItem.totals?.subTotal?.value ?? 0) / (orderItem.quantity || 0), diff --git a/packages/react/src/components/storefront/cart.tsx b/packages/react/src/components/storefront/cart.tsx index 58a8943e..9d0472ff 100644 --- a/packages/react/src/components/storefront/cart.tsx +++ b/packages/react/src/components/storefront/cart.tsx @@ -1,55 +1,131 @@ 'use client'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { Loader2, ShoppingCart } from 'lucide-react'; +import { useEffect, useState } from 'react'; import type { Product } from '@/components/checkout/line-items/line-items'; import { CartLineItems } from '@/components/storefront/cart-line-items'; import { CartTotals } from '@/components/storefront/cart-totals'; +import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, + SheetFooter, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { useGoDaddyContext } from '@/godaddy-provider'; +import { getCartOrderId } from '@/lib/cart-storage'; +import { deleteCartLineItem, getCartOrder } from '@/lib/godaddy/godaddy'; interface CartProps { open: boolean; onOpenChange: (open: boolean) => void; + onCheckout?: (orderId: string) => void; + isCheckingOut?: boolean; } -export function Cart({ open, onOpenChange }: CartProps) { - // Mock data - const items: Product[] = [ - { - id: 'LineItem_2y0l7o6Oi4BW6fpSiKPX1hhBccU', - name: 'Box of cookies', - image: - 'https://isteam.dev-wsimg.com/ip/2f2e05ec-de6f-4a89-90f2-038c749655b0/cookies.webp', - quantity: 2, - originalPrice: 10.99, - price: 10.99, - notes: [], - }, - { - id: 'LineItem_2y0l9FykA04qp2pC6y3YZ0TbZFD', - name: 'Cupcakes', - image: - 'https://isteam.dev-wsimg.com/ip/2f2e05ec-de6f-4a89-90f2-038c749655b0/cupcakes.webp/:/rs=w:600,h:600', - quantity: 1, - originalPrice: 5.99, - price: 5.99, - notes: [], +export function Cart({ + open, + onOpenChange, + onCheckout, + isCheckingOut = false, +}: CartProps) { + const context = useGoDaddyContext(); + const queryClient = useQueryClient(); + const [cartOrderId, setCartOrderId] = useState(null); + + useEffect(() => { + setCartOrderId(getCartOrderId()); + }, []); + + // Fetch cart order + const { + data: cartData, + isLoading, + error, + } = useQuery({ + queryKey: ['cart-order', cartOrderId], + queryFn: () => + getCartOrder( + cartOrderId!, + context.storeId!, + context.clientId!, + context?.apiHost + ), + enabled: !!cartOrderId && !!context.storeId && !!context.clientId, + }); + + // Delete line item mutation + const deleteMutation = useMutation({ + mutationFn: (lineItemId: string) => + deleteCartLineItem( + { id: lineItemId, orderId: cartOrderId! }, + context.storeId!, + context.clientId!, + context?.apiHost + ), + onSuccess: () => { + // Invalidate cart query to refetch + queryClient.invalidateQueries({ queryKey: ['cart-order', cartOrderId] }); }, - ]; + }); + + const handleRemoveFromCart = (itemId: string) => { + deleteMutation.mutate(itemId); + }; + + const order = cartData?.orderById; + + const { t } = useGoDaddyContext(); + + // Transform cart line items to Product format for CartLineItems component + const items: Product[] = + order?.lineItems?.map(item => ({ + id: item.id, + name: item.name || t.storefront.product, + image: item.details?.productAssetUrl || '', + quantity: item.quantity || 0, + originalPrice: (item.totals?.subTotal?.value || 0) / (item.quantity || 1), + price: (item.totals?.subTotal?.value || 0) / (item.quantity || 1), + selectedOptions: + item?.details?.selectedOptions?.map(option => ({ + attribute: option.attribute || '', + values: option.values || [], + })) || [], + addons: item.details?.selectedAddons?.map(addon => ({ + attribute: addon.attribute || '', + sku: addon.sku || '', + values: addon.values?.map(value => ({ + costAdjustment: value.costAdjustment + ? { + currencyCode: value.costAdjustment.currencyCode ?? undefined, + value: value.costAdjustment.value ?? undefined, + } + : undefined, + name: value.name ?? undefined, + })), + })), + })) || []; + + // Calculate totals + const itemCount = items.reduce((sum, item) => sum + item.quantity, 0); + const currencyCode = order?.totals?.total?.currencyCode || 'USD'; + const subtotal = order?.totals?.subTotal?.value || 0; + const shipping = order?.totals?.shippingTotal?.value || 0; + const taxes = order?.totals?.taxTotal?.value || 0; + const discount = order?.totals?.discountTotal?.value || 0; + const total = order?.totals?.total?.value || 0; const totals = { - subtotal: 27.97, - discount: 0, - shipping: 0, - currencyCode: 'USD', - itemCount: 3, - total: 27.97, + subtotal, + discount, + shipping, + currencyCode, + itemCount, + total, tip: 0, - taxes: 0, + taxes, enableDiscounts: false, enableTaxes: true, isTaxLoading: false, @@ -59,11 +135,79 @@ export function Cart({ open, onOpenChange }: CartProps) { - Shopping Cart + {t.storefront.shoppingCart}
- - + {isLoading && ( +
+ +
+ )} + + {!isLoading && error && ( +
+

+ {t.storefront.failedToLoadCart} {(error as Error).message} +

+ +
+ )} + + {!isLoading && !error && (!cartOrderId || items.length === 0) && ( +
+ +

+ {t.storefront.yourCartIsEmpty} +

+

+ {t.storefront.addItemsToGetStarted} +

+
+ )} + + {!isLoading && !error && cartOrderId && items.length > 0 && ( + <> + + + {onCheckout ? ( + + + + ) : null} + + )}
diff --git a/packages/react/src/components/storefront/hooks/use-add-to-cart.ts b/packages/react/src/components/storefront/hooks/use-add-to-cart.ts new file mode 100644 index 00000000..34c9d01c --- /dev/null +++ b/packages/react/src/components/storefront/hooks/use-add-to-cart.ts @@ -0,0 +1,120 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useGoDaddyContext } from '@/godaddy-provider'; +import { getCartOrderId, setCartOrderId } from '@/lib/cart-storage'; +import { addCartLineItem, createCartOrder } from '@/lib/godaddy/godaddy'; + +export interface AddToCartInput { + skuId: string; + name: string; + quantity: number; + productAssetUrl?: string; +} + +export interface UseAddToCartOptions { + onSuccess?: () => void; + onError?: (error: Error) => void; +} + +export function useAddToCart(options?: UseAddToCartOptions) { + const context = useGoDaddyContext(); + const queryClient = useQueryClient(); + + // Create cart order mutation + const createCartMutation = useMutation({ + mutationFn: () => + createCartOrder( + { + context: { + storeId: context?.storeId || '', + channelId: context?.channelId || '', + }, + totals: { + subTotal: { value: 0, currencyCode: 'USD' }, + shippingTotal: { value: 0, currencyCode: 'USD' }, + discountTotal: { value: 0, currencyCode: 'USD' }, + feeTotal: { value: 0, currencyCode: 'USD' }, + taxTotal: { value: 0, currencyCode: 'USD' }, + total: { value: 0, currencyCode: 'USD' }, + }, + }, + context.storeId!, + context.clientId!, + context?.apiHost + ), + onSuccess: data => { + if (data.addDraftOrder?.id) { + setCartOrderId(data.addDraftOrder.id); + } + }, + }); + + // Add line item mutation + const addLineItemMutation = useMutation({ + mutationFn: ({ + orderId, + input, + }: { + orderId: string; + input: AddToCartInput; + }) => + addCartLineItem( + { + orderId, + skuId: input.skuId, + name: input.name, + quantity: input.quantity, + fulfillmentMode: 'SHIP', + status: 'DRAFT', + details: { + productAssetUrl: input.productAssetUrl || undefined, + }, + }, + context.storeId!, + context.clientId!, + context?.apiHost + ), + onSuccess: () => { + // Invalidate cart query to refresh + const cartOrderId = getCartOrderId(); + queryClient.invalidateQueries({ queryKey: ['cart-order', cartOrderId] }); + + // Call success callback + options?.onSuccess?.(); + }, + onError: error => { + // Call error callback + options?.onError?.(error as Error); + }, + }); + + const addToCart = async (input: AddToCartInput) => { + if (!context.storeId || !context.clientId) { + const error = new Error('Store ID and Client ID are required'); + options?.onError?.(error); + throw error; + } + let cartOrderId = getCartOrderId(); + + // Create cart if it doesn't exist + if (!cartOrderId) { + const result = await createCartMutation.mutateAsync(); + cartOrderId = result.addDraftOrder?.id || null; + + if (!cartOrderId) { + const error = new Error('Failed to create cart'); + options?.onError?.(error); + throw error; + } + } + + // Add line item to cart + await addLineItemMutation.mutateAsync({ orderId: cartOrderId, input }); + }; + + return { + addToCart, + isLoading: createCartMutation.isPending || addLineItemMutation.isPending, + isCreatingCart: createCartMutation.isPending, + isAddingItem: addLineItemMutation.isPending, + }; +} diff --git a/packages/react/src/components/storefront/product-card.tsx b/packages/react/src/components/storefront/product-card.tsx index 0b642ec1..107d6442 100644 --- a/packages/react/src/components/storefront/product-card.tsx +++ b/packages/react/src/components/storefront/product-card.tsx @@ -1,21 +1,31 @@ 'use client'; -import { ChevronRight, ShoppingBag } from 'lucide-react'; +import { ChevronRight, Loader2, ShoppingBag } from 'lucide-react'; import { useFormatCurrency } from '@/components/checkout/utils/format-currency'; +import { useAddToCart } from '@/components/storefront/hooks/use-add-to-cart'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; import { RouterLink } from '@/components/ui/link'; +import { useGoDaddyContext } from '@/godaddy-provider'; import { SKUGroup } from '@/types.ts'; interface ProductCardProps { product: SKUGroup; href?: string; + onAddToCartSuccess?: () => void; + onAddToCartError?: (error: Error) => void; } -export function ProductCard({ product, href }: ProductCardProps) { +export function ProductCard({ + product, + href, + onAddToCartSuccess, + onAddToCartError, +}: ProductCardProps) { const formatCurrency = useFormatCurrency(); - const title = product?.label || product?.name || 'Product'; + const { t } = useGoDaddyContext(); + const title = product?.label || product?.name || t.storefront.product; const description = product?.description || ''; const priceMin = product?.priceRange?.min || 0; const priceMax = product?.priceRange?.max || priceMin; @@ -28,16 +38,99 @@ export function ProductCard({ product, href }: ProductCardProps) { edge => edge?.node?.type === 'IMAGE' )?.node?.url; - const handleAddToCart = (e: React.MouseEvent) => { + // Get first SKU and check inventory + const firstSku = product?.skus?.edges?.[0]?.node; + const skuId = firstSku?.id || product?.id || ''; + + // Check available inventory for first SKU + const inventoryCounts = firstSku?.inventoryCounts?.edges || []; + const availableInventory = + inventoryCounts.find((edge: any) => edge?.node?.type === 'AVAILABLE')?.node + ?.quantity || 0; + + const isFirstSkuInStock = availableInventory > 0; + const hasMultipleSkus = (product?.skus?.edges?.length || 0) > 1; + + // Use shared add to cart hook + const { addToCart, isLoading: isAddingToCart } = useAddToCart({ + onSuccess: onAddToCartSuccess, + onError: onAddToCartError, + }); + + const handleAddToCart = async (e: React.MouseEvent) => { e.preventDefault(); + e.stopPropagation(); + + if (!skuId) { + return; + } + + await addToCart({ + skuId, + name: title, + quantity: 1, + productAssetUrl: imageUrl || undefined, + }); + }; + + // Determine which button to show based on inventory and variants + const getActionButton = () => { + // If product has options (future use case), show "Select Options" + if (hasOptions) { + return ( + + ); + } + + // If first SKU is out of stock + if (!isFirstSkuInStock) { + // If there are multiple SKUs and an href, suggest viewing details + if (hasMultipleSkus && href) { + return ( + + ); + } + // Otherwise show out of stock button + return ( + + ); + } + + // First SKU is in stock, show "Add to Cart" + return ( + + ); }; const cardContent = ( <>
{isOnSale && ( - - SALE + + {t.storefront.sale} )} {imageUrl ? ( @@ -48,7 +141,7 @@ export function ProductCard({ product, href }: ProductCardProps) { /> ) : (
- No image + {t.storefront.noImage}
)}
@@ -69,17 +162,7 @@ export function ProductCard({ product, href }: ProductCardProps) { inputInMinorUnits: true, })}
- {hasOptions ? ( - - ) : ( - - )} + {getActionButton()}
diff --git a/packages/react/src/components/storefront/product-details.tsx b/packages/react/src/components/storefront/product-details.tsx index 31201a4a..7e072ca8 100644 --- a/packages/react/src/components/storefront/product-details.tsx +++ b/packages/react/src/components/storefront/product-details.tsx @@ -1,9 +1,10 @@ 'use client'; import { useQuery } from '@tanstack/react-query'; -import { Minus, Plus, ShoppingCart } from 'lucide-react'; +import { Loader2, Minus, Plus, ShoppingCart } from 'lucide-react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useFormatCurrency } from '@/components/checkout/utils/format-currency'; +import { useAddToCart } from '@/components/storefront/hooks/use-add-to-cart'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card } from '@/components/ui/card'; @@ -24,6 +25,8 @@ interface ProductDetailsProps { productId: string; storeId?: string; clientId?: string; + onAddToCartSuccess?: () => void; + onAddToCartError?: (error: Error) => void; } // Flattened attribute structure for UI (transforms edges/node to flat array) @@ -119,8 +122,11 @@ export function ProductDetails({ productId, storeId: storeIdProp, clientId: clientIdProp, + onAddToCartSuccess, + onAddToCartError, }: ProductDetailsProps) { const context = useGoDaddyContext(); + const { t } = context; const formatCurrency = useFormatCurrency(); // Props take priority over context values @@ -145,6 +151,15 @@ export function ProductDetails({ return result; }); + // Use shared add to cart hook + const { addToCart, isLoading: isAddingToCart } = useAddToCart({ + onSuccess: () => { + setQuantity(1); // Reset quantity + onAddToCartSuccess?.(); + }, + onError: onAddToCartError, + }); + // Update URL when variant params change const setVariantParams = useCallback( (updates: Record) => { @@ -215,7 +230,11 @@ export function ProductDetails({ ], queryFn: () => getSkuGroup( - { id: productId!, attributeValues: selectedAttributeValues }, + { + id: productId!, + attributeValues: selectedAttributeValues, + ...(!selectedAttributeValues.length ? { first: 2 } : {}), + }, storeId!, clientId!, context?.apiHost @@ -297,7 +316,7 @@ export function ProductDetails({ return (
- Error loading product: {error.message} + {t.storefront.errorLoadingProduct} {error.message}
); @@ -309,13 +328,13 @@ export function ProductDetails({ return (
- Product not found + {t.storefront.productNotFound}
); } - const title = product?.label || product?.name || 'Product'; + const title = product?.label || product?.name || t.storefront.product; const description = product?.description || ''; const htmlDescription = product?.htmlDescription || ''; @@ -376,10 +395,19 @@ export function ProductDetails({ })() : false; - const canAddToCart = !isOutOfStock && (!attributes.length || selectedSku); + const canAddToCart = !isOutOfStock && Boolean(selectedSku); + + const handleAddToCart = async () => { + if (!canAddToCart) { + return; + } - const handleAddToCart = () => { - // Placeholder for add to cart functionality + await addToCart({ + skuId: selectedSku?.id || product?.skus?.edges?.[0]?.node?.id || '', + name: title, + quantity, + productAssetUrl: images[0] || undefined, + }); }; return ( @@ -389,8 +417,11 @@ export function ProductDetails({ {/* Main Image Carousel */}
{isOnSale && ( - - SALE + + {t.storefront.sale} )}
- No image available + {t.storefront.noImageAvailable}
@@ -501,7 +532,7 @@ export function ProductDetails({
{/* Product Information */} -
+

{title}

@@ -580,19 +611,17 @@ export function ProductDetails({ {isSkuLoading && (
- Loading variant details... + {t.storefront.loadingVariantDetails}
)} {!isSkuLoading && matchedSkus.length === 0 && (
- This combination is not available. Please select different - options. + {t.storefront.combinationNotAvailable}
)} {!isSkuLoading && matchedSkus.length > 1 && (
- {matchedSkus.length} variants match your selection. Select - more attributes to narrow down. + {matchedSkus.length} {t.storefront.variantsMatch}
)}
@@ -603,7 +632,7 @@ export function ProductDetails({ {/* Quantity Selector */}
{/* Additional Product Information */}
{product?.type && (
- Product Type: + + {t.storefront.productType} + {product.type} @@ -650,7 +690,9 @@ export function ProductDetails({ )} {product?.id && (
- Product ID: + + {t.storefront.productId} + {product.id} @@ -659,7 +701,9 @@ export function ProductDetails({ {selectedSku && ( <>
- Selected SKU: + + {t.storefront.selectedSku} + {selectedSku.code} @@ -667,17 +711,20 @@ export function ProductDetails({ {selectedSku.inventoryCounts?.edges && selectedSku.inventoryCounts.edges.length > 0 && (
- Stock Status: + + {t.storefront.stockStatus} + {(() => { const availableCount = selectedSku.inventoryCounts.edges.find( edge => edge?.node?.type === 'AVAILABLE' )?.node?.quantity ?? 0; - if (availableCount === 0) return 'Out of Stock'; + if (availableCount === 0) + return t.storefront.outOfStock; if (availableCount < 10) - return `Low Stock (${availableCount})`; - return 'In Stock'; + return `${t.storefront.lowStock} (${availableCount})`; + return t.storefront.inStock; })()}
diff --git a/packages/react/src/components/storefront/product-grid.tsx b/packages/react/src/components/storefront/product-grid.tsx index dc0fe042..d73607f0 100644 --- a/packages/react/src/components/storefront/product-grid.tsx +++ b/packages/react/src/components/storefront/product-grid.tsx @@ -11,6 +11,8 @@ interface ProductGridProps { clientId?: string; first?: number; getProductHref?: (productId: string) => string; + onAddToCartSuccess?: () => void; + onAddToCartError?: (error: Error) => void; } function ProductGridSkeleton({ count = 6 }: { count?: number }) { @@ -34,8 +36,11 @@ export function ProductGrid({ clientId: clientIdProp, first = 100, getProductHref, + onAddToCartSuccess, + onAddToCartError, }: ProductGridProps) { const context = useGoDaddyContext(); + const { t } = context; const storeId = storeIdProp || context.storeId; const clientId = clientIdProp || context.clientId; @@ -51,7 +56,11 @@ export function ProductGrid({ } if (error) { - return
Error loading products: {error.message}
; + return ( +
+ {t.storefront.errorLoadingProducts} {error.message} +
+ ); } const skuGroups = data?.skuGroups?.edges; @@ -63,7 +72,15 @@ export function ProductGrid({ if (!group?.id) return null; const href = getProductHref?.(group.id); - return ; + return ( + + ); })}
); diff --git a/packages/react/src/components/ui/badge.tsx b/packages/react/src/components/ui/badge.tsx index 19b5addc..47d4ca2f 100644 --- a/packages/react/src/components/ui/badge.tsx +++ b/packages/react/src/components/ui/badge.tsx @@ -13,6 +13,8 @@ const badgeVariants = cva( 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80', destructive: 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80', + accent: + 'border-transparent bg-accent text-accent-foreground hover:bg-accent/80', outline: 'text-foreground', }, }, diff --git a/packages/react/src/components/ui/sheet.tsx b/packages/react/src/components/ui/sheet.tsx index 408e6676..f867253c 100644 --- a/packages/react/src/components/ui/sheet.tsx +++ b/packages/react/src/components/ui/sheet.tsx @@ -68,7 +68,7 @@ const SheetContent = React.forwardRef< className={cn(sheetVariants({ side }), className)} {...props} > - + {t.ui.accessibility.close} diff --git a/packages/react/src/globals.css b/packages/react/src/globals.css index 89152395..ac3016a4 100644 --- a/packages/react/src/globals.css +++ b/packages/react/src/globals.css @@ -1,6 +1,80 @@ @import "tailwindcss" source(none); @source "./components/"; +@keyframes enter { + from { + opacity: 0; + transform: var(--tw-enter-transform); + } +} + +@keyframes exit { + to { + opacity: 0; + transform: var(--tw-exit-transform); + } +} + +@utility animate-in { + animation-name: enter; + animation-duration: 0.15s; + --tw-enter-transform: none; +} + +@utility animate-out { + animation-name: exit; + animation-duration: 0.15s; + --tw-exit-transform: none; +} + +@utility fade-in-0 { + --tw-enter-opacity: 0; +} + +@utility fade-out-0 { + --tw-exit-opacity: 0; +} + +@utility slide-in-from-right { + --tw-enter-transform: translateX(100%); +} + +@utility slide-out-to-right { + --tw-exit-transform: translateX(100%); +} + +@utility slide-in-from-left { + --tw-enter-transform: translateX(-100%); +} + +@utility slide-out-to-left { + --tw-exit-transform: translateX(-100%); +} + +@utility slide-in-from-top { + --tw-enter-transform: translateY(-100%); +} + +@utility slide-out-to-top { + --tw-exit-transform: translateY(-100%); +} + +@utility slide-in-from-bottom { + --tw-enter-transform: translateY(100%); +} + +@utility slide-out-to-bottom { + --tw-exit-transform: translateY(100%); +} + +@utility duration-300 { + animation-duration: 300ms; +} + +@utility duration-500 { + animation-duration: 500ms; +} + @font-face { font-family: "GD Sherpa"; src: url("https://d85ecz8votkqa.cloudfront.net/fonts/gd-sherpa-regular.eot"); /* IE9 Compat Modes */ diff --git a/packages/react/src/godaddy-provider.tsx b/packages/react/src/godaddy-provider.tsx index 6faea256..c13fd245 100644 --- a/packages/react/src/godaddy-provider.tsx +++ b/packages/react/src/godaddy-provider.tsx @@ -73,6 +73,7 @@ interface GoDaddyContextValue { apiHost?: string; clientId?: string; storeId?: string; + channelId?: string; locale?: string; Link?: React.ComponentType; } @@ -101,13 +102,13 @@ export interface GoDaddyProviderProps { * * Internal devs can set to: * - "http://localhost:3000" for local development - * - "https://checkout.commerce.api.dev-godaddy.com" for DEV environment - * - "https://checkout.commerce.api.test-godaddy.com" for TEST environment */ apiHost?: string; clientId?: string; storeId?: string; + channelId?: string; locale?: string; + queryClient?: QueryClient; Link?: React.ComponentType; children: QueryClientProviderProps['children']; @@ -120,6 +121,7 @@ export function GoDaddyProvider({ apiHost, clientId, storeId, + channelId, locale = 'en-US', queryClient: providedQueryClient, Link, @@ -214,6 +216,7 @@ export function GoDaddyProvider({ apiHost, clientId, storeId, + channelId, locale, Link, }} diff --git a/packages/react/src/lib/cart-storage.ts b/packages/react/src/lib/cart-storage.ts new file mode 100644 index 00000000..3f4c2e2b --- /dev/null +++ b/packages/react/src/lib/cart-storage.ts @@ -0,0 +1,78 @@ +const CART_ORDER_ID_KEY = 'godaddy_cart_order_id'; +const CART_CREATED_AT_KEY = 'godaddy_cart_created_at'; +const CART_TTL = 30 * 24 * 60 * 60 * 1000; // 30 days in milliseconds + +/** + * Get the cart order ID from localStorage. + * Returns null if the cart doesn't exist or has expired. + */ +export function getCartOrderId(): string | null { + if (typeof window === 'undefined') { + // SSR safety + return null; + } + + const orderId = localStorage.getItem(CART_ORDER_ID_KEY); + const createdAt = localStorage.getItem(CART_CREATED_AT_KEY); + + // No cart exists + if (!orderId) { + return null; + } + + // Check if cart has expired (30 days old) + if (createdAt) { + const age = Date.now() - parseInt(createdAt, 10); + if (age > CART_TTL) { + // Cart expired, clean it up + clearCartOrderId(); + return null; + } + } + + return orderId; +} + +/** + * Save the cart order ID to localStorage with a timestamp. + */ +export function setCartOrderId(orderId: string): void { + if (typeof window === 'undefined') { + // SSR safety + return; + } + + localStorage.setItem(CART_ORDER_ID_KEY, orderId); + localStorage.setItem(CART_CREATED_AT_KEY, Date.now().toString()); +} + +/** + * Remove the cart order ID and timestamp from localStorage. + */ +export function clearCartOrderId(): void { + if (typeof window === 'undefined') { + // SSR safety + return; + } + + localStorage.removeItem(CART_ORDER_ID_KEY); + localStorage.removeItem(CART_CREATED_AT_KEY); +} + +/** + * Get the age of the current cart in days. + * Returns null if no cart exists. + */ +export function getCartAge(): number | null { + if (typeof window === 'undefined') { + return null; + } + + const createdAt = localStorage.getItem(CART_CREATED_AT_KEY); + if (!createdAt) { + return null; + } + + const ageMs = Date.now() - parseInt(createdAt, 10); + return Math.floor(ageMs / (24 * 60 * 60 * 1000)); // Convert to days +} diff --git a/packages/react/src/lib/godaddy/catalog-storefront-env.ts b/packages/react/src/lib/godaddy/catalog-storefront-env.ts index dcce0046..5d61fca3 100644 --- a/packages/react/src/lib/godaddy/catalog-storefront-env.ts +++ b/packages/react/src/lib/godaddy/catalog-storefront-env.ts @@ -1325,15 +1325,6 @@ const introspection = { ], isDeprecated: false, }, - { - name: 'status', - type: { - kind: 'SCALAR', - name: 'String', - }, - args: [], - isDeprecated: false, - }, { name: 'updatedAt', type: { @@ -2528,15 +2519,6 @@ const introspection = { ], isDeprecated: false, }, - { - name: 'status', - type: { - kind: 'SCALAR', - name: 'String', - }, - args: [], - isDeprecated: false, - }, { name: 'updatedAt', type: { @@ -8454,6 +8436,24 @@ const introspection = { args: [], isDeprecated: true, }, + { + name: 'skuCount', + type: { + kind: 'SCALAR', + name: 'Int', + }, + args: [ + { + name: 'status', + type: { + kind: 'INPUT_OBJECT', + name: 'SKUStatusFilter', + }, + defaultValue: '{in: ["ACTIVE", "DRAFT"]}', + }, + ], + isDeprecated: false, + }, { name: 'skus', type: { diff --git a/packages/react/src/lib/godaddy/catalog-storefront-queries.ts b/packages/react/src/lib/godaddy/catalog-storefront-queries.ts index fc1f4b02..eab1ed43 100644 --- a/packages/react/src/lib/godaddy/catalog-storefront-queries.ts +++ b/packages/react/src/lib/godaddy/catalog-storefront-queries.ts @@ -1,7 +1,7 @@ import { graphql } from '@/lib/gql/gql-catalog-storefront.tada.ts'; export const SkuGroupsQuery = graphql(` - query SkuGroups($first: Int, $after: String, $id: SKUGroupIdsFilter, $attributeValues: [String!] = []) { + query SkuGroups($first: Int, $after: String, $id: SKUGroupIdsFilter) { skuGroups(first: $first, after: $after, id: $id) { edges { cursor @@ -48,12 +48,21 @@ export const SkuGroupsQuery = graphql(` } } } - skus(attributeValues: { has: $attributeValues }) { + skus(first: 2) { edges { node { id label name + inventoryCounts { + edges { + node { + id + quantity + type + } + } + } } } } @@ -70,7 +79,7 @@ export const SkuGroupsQuery = graphql(` `); export const SkuGroupQuery = graphql(` - query SkuGroup($id: String!, $attributeValues: [String!] = []) { + query SkuGroup($id: String!, $first: Int, $attributeValues: [String!] = []) { skuGroup(id: $id) { id name @@ -114,12 +123,24 @@ export const SkuGroupQuery = graphql(` } } } - skus(attributeValues: { has: $attributeValues }) { + skus( + attributeValues: { has: $attributeValues } + first: $first + ) { edges { node { id label name + inventoryCounts { + edges { + node { + id + quantity + type + } + } + } } } } diff --git a/packages/react/src/lib/godaddy/checkout-mutations.ts b/packages/react/src/lib/godaddy/checkout-mutations.ts index 23cc9741..d493425c 100644 --- a/packages/react/src/lib/godaddy/checkout-mutations.ts +++ b/packages/react/src/lib/godaddy/checkout-mutations.ts @@ -5,6 +5,7 @@ export const CreateCheckoutSessionMutation = graphql(` createCheckoutSession(input: $input) { id token + url sourceApp returnUrl successUrl diff --git a/packages/react/src/lib/godaddy/godaddy.ts b/packages/react/src/lib/godaddy/godaddy.ts index ece3f3ab..5d45662b 100644 --- a/packages/react/src/lib/godaddy/godaddy.ts +++ b/packages/react/src/lib/godaddy/godaddy.ts @@ -6,15 +6,28 @@ import { SkuGroupsQuery, SkuQuery, } from '@/lib/godaddy/catalog-storefront-queries.ts'; +import { + AddCartOrderMutation, + AddLineItemBySkuIdMutation, + ApplyDiscountCodesMutation, + DeleteLineItemByIdMutation, + UpdateCartOrderMutation, + UpdateLineItemByIdMutation, +} from '@/lib/godaddy/orders-storefront-mutations.ts'; +import { GetCartOrderQuery } from '@/lib/godaddy/orders-storefront-queries.ts'; import { graphqlRequestWithErrors } from '@/lib/graphql-with-errors'; import type { + AddCartOrderInput, + AddLineItemBySkuIdInput, ApplyCheckoutSessionDeliveryMethodInput, ApplyCheckoutSessionDiscountInput, ApplyCheckoutSessionFulfillmentLocationInput, ApplyCheckoutSessionShippingMethodInput, + ApplyDiscountCodesInput, CheckoutSession, CheckoutSessionInput, ConfirmCheckoutMutationInput, + DeleteLineItemByIdInput, DraftOrderPriceAdjustmentsQueryInput, GetCheckoutSessionShippingRatesInput, GetCheckoutSessionTaxesInput, @@ -22,7 +35,9 @@ import type { SkuGroupInput, SkuGroupsInput, SkuInput, + UpdateCartOrderInput, UpdateDraftOrderInput, + UpdateLineItemByIdInput, } from '@/types'; import { ApplyCheckoutSessionDeliveryMethodMutation, @@ -48,9 +63,13 @@ import { GetCheckoutSessionQuery, } from './checkout-queries.ts'; -function getHostByEnvironment(apiHost?: string, service = 'checkout'): string { +function getHostByEnvironment(apiHost?: string): string { // Use provided apiHost, otherwise default to production - return `https://${service}.commerce.${apiHost || 'api.godaddy.com'}`; + return `https://checkout.commerce.${apiHost || 'api.godaddy.com'}`; +} + +function getApiHostByEnvironment(apiHost?: string, endpoint?: string): string { + return `https://${apiHost || 'api.godaddy.com'}${endpoint ? endpoint : ''}`; } // Type for createCheckoutSession input with kebab-case appearance @@ -1066,10 +1085,13 @@ export function getSkuGroups( clientId: string, apiHost?: string ) { - const GODADDY_HOST = getHostByEnvironment(apiHost, 'catalog'); + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v2/commerce/stores/${storeId}/catalog-subgraph/storefront` + ); return graphqlRequestWithErrors>( - `${GODADDY_HOST}/storefront`, + GODADDY_HOST, SkuGroupsQuery, input, { @@ -1085,10 +1107,13 @@ export function getSkuGroup( clientId: string, apiHost?: string ) { - const GODADDY_HOST = getHostByEnvironment(apiHost, 'catalog'); + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v2/commerce/stores/${storeId}/catalog-subgraph/storefront` + ); return graphqlRequestWithErrors>( - `${GODADDY_HOST}/storefront`, + GODADDY_HOST, SkuGroupQuery, input, { @@ -1104,10 +1129,13 @@ export function getSku( clientId: string, apiHost?: string ) { - const GODADDY_HOST = getHostByEnvironment(apiHost, 'catalog'); + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v2/commerce/stores/${storeId}/catalog-subgraph/storefront` + ); return graphqlRequestWithErrors>( - `${GODADDY_HOST}/storefront`, + GODADDY_HOST, SkuQuery, input, { @@ -1116,3 +1144,157 @@ export function getSku( } ); } + +// Cart/Orders functions +export function createCartOrder( + input: AddCartOrderInput, + storeId: string, + clientId: string, + apiHost?: string +) { + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v1/commerce/order-storefront-subgraph` + ); + + return graphqlRequestWithErrors>( + GODADDY_HOST, + AddCartOrderMutation, + { input }, + { + 'X-Store-ID': storeId, + 'X-Client-ID': clientId, + } + ); +} + +export function getCartOrder( + orderId: string, + storeId: string, + clientId: string, + apiHost?: string +) { + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v1/commerce/order-storefront-subgraph` + ); + + return graphqlRequestWithErrors>( + GODADDY_HOST, + GetCartOrderQuery, + { id: orderId }, + { + 'X-Store-ID': storeId, + 'X-Client-ID': clientId, + } + ); +} + +export function addCartLineItem( + input: AddLineItemBySkuIdInput, + storeId: string, + clientId: string, + apiHost?: string +) { + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v1/commerce/order-storefront-subgraph` + ); + + return graphqlRequestWithErrors>( + GODADDY_HOST, + AddLineItemBySkuIdMutation, + { input }, + { + 'X-Store-ID': storeId, + 'X-Client-ID': clientId, + } + ); +} + +export function updateCartOrder( + input: UpdateCartOrderInput, + storeId: string, + clientId: string, + apiHost?: string +) { + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v1/commerce/order-storefront-subgraph` + ); + + return graphqlRequestWithErrors>( + GODADDY_HOST, + UpdateCartOrderMutation, + { input }, + { + 'X-Store-ID': storeId, + 'X-Client-ID': clientId, + } + ); +} + +export function deleteCartLineItem( + input: DeleteLineItemByIdInput, + storeId: string, + clientId: string, + apiHost?: string +) { + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v1/commerce/order-storefront-subgraph` + ); + + return graphqlRequestWithErrors>( + GODADDY_HOST, + DeleteLineItemByIdMutation, + input, + { + 'X-Store-ID': storeId, + 'X-Client-ID': clientId, + } + ); +} + +export function applyCartDiscountCodes( + input: ApplyDiscountCodesInput, + storeId: string, + clientId: string, + apiHost?: string +) { + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v1/commerce/order-storefront-subgraph` + ); + return graphqlRequestWithErrors>( + GODADDY_HOST, + ApplyDiscountCodesMutation, + { input }, + { + 'X-Store-ID': storeId, + 'X-Client-ID': clientId, + } + ); +} + +export function updateCartLineItem( + input: UpdateLineItemByIdInput, + storeId: string, + clientId: string, + apiHost?: string +) { + const GODADDY_HOST = getApiHostByEnvironment( + apiHost, + `/v1/commerce/order-storefront-subgraph` + ); + + return graphqlRequestWithErrors>( + GODADDY_HOST, + UpdateLineItemByIdMutation, + { input }, + { + 'X-Store-ID': storeId, + 'X-Client-ID': clientId, + } + ); +} diff --git a/packages/react/src/lib/godaddy/orders-storefront-env.ts b/packages/react/src/lib/godaddy/orders-storefront-env.ts index 3949dcaf..c35d6566 100644 --- a/packages/react/src/lib/godaddy/orders-storefront-env.ts +++ b/packages/react/src/lib/godaddy/orders-storefront-env.ts @@ -64,11 +64,8 @@ const introspection = { { name: 'totals', type: { - kind: 'NON_NULL', - ofType: { - kind: 'INPUT_OBJECT', - name: 'OrderTotalsInput', - }, + kind: 'INPUT_OBJECT', + name: 'OrderTotalsInput', }, }, { @@ -1326,6 +1323,15 @@ const introspection = { args: [], isDeprecated: false, }, + { + name: 'unitAmount', + type: { + kind: 'OBJECT', + name: 'Money', + }, + args: [], + isDeprecated: false, + }, { name: 'updatedAt', type: { diff --git a/packages/react/src/lib/godaddy/orders-storefront-mutations.ts b/packages/react/src/lib/godaddy/orders-storefront-mutations.ts new file mode 100644 index 00000000..f485ce0f --- /dev/null +++ b/packages/react/src/lib/godaddy/orders-storefront-mutations.ts @@ -0,0 +1,302 @@ +import { graphql } from '@/lib/gql/gql-orders-storefront.tada'; + +export const AddCartOrderMutation = graphql(` + mutation AddCartOrder($input: AddDraftOrderInput!) { + addDraftOrder(input: $input) { + id + customerId + createdAt + updatedAt + context { + storeId + channelId + } + lineItems { + id + name + quantity + skuId + type + fulfillmentMode + totals { + subTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + } + } + totals { + subTotal { + value + currencyCode + } + shippingTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + total { + value + currencyCode + } + } + } + } +`); + +export const AddLineItemBySkuIdMutation = graphql(` + mutation AddLineItemBySkuId($input: AddLineItemInput!) { + addLineItemBySkuId(input: $input) { + id + name + quantity + skuId + type + fulfillmentMode + details { + productAssetUrl + sku + unitOfMeasure + selectedOptions { + attribute + values + } + selectedAddons { + attribute + sku + values { + name + costAdjustment { + value + currencyCode + } + } + } + } + totals { + subTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + } + discounts { + id + name + code + amount { + value + currencyCode + } + } + taxes { + id + name + amount { + value + currencyCode + } + ratePercentage + } + createdAt + updatedAt + } + } +`); + +export const UpdateCartOrderMutation = graphql(` + mutation UpdateCartOrder($input: UpdateDraftOrderInput!) { + updateDraftOrder(input: $input) { + id + customerId + createdAt + updatedAt + context { + storeId + channelId + } + lineItems { + id + name + quantity + skuId + type + fulfillmentMode + totals { + subTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + } + } + totals { + subTotal { + value + currencyCode + } + shippingTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + total { + value + currencyCode + } + } + } + } +`); + +export const DeleteLineItemByIdMutation = graphql(` + mutation DeleteLineItemById($id: ID!, $orderId: ID!) { + deleteLineItemById(id: $id, orderId: $orderId) + } +`); + +export const ApplyDiscountCodesMutation = graphql(` + mutation ApplyDiscountCodes($input: ApplyDiscountCodesInput!) { + applyDiscountCodes(input: $input) { + id + discounts { + id + name + code + amount { + value + currencyCode + } + ratePercentage + appliedBeforeTax + } + totals { + subTotal { + value + currencyCode + } + shippingTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + productDiscountTotal { + value + currencyCode + } + shippingDiscountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + total { + value + currencyCode + } + } + } + } +`); + +export const UpdateLineItemByIdMutation = graphql(` + mutation UpdateLineItemById($input: UpdateLineItemByIdInput!) { + updateLineItemById(input: $input) { + id + name + quantity + skuId + type + fulfillmentMode + details { + productAssetUrl + sku + unitOfMeasure + } + totals { + subTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + } + updatedAt + } + } +`); diff --git a/packages/react/src/lib/godaddy/orders-storefront-queries.ts b/packages/react/src/lib/godaddy/orders-storefront-queries.ts new file mode 100644 index 00000000..4a4dcf4c --- /dev/null +++ b/packages/react/src/lib/godaddy/orders-storefront-queries.ts @@ -0,0 +1,169 @@ +import { graphql } from '@/lib/gql/gql-orders-storefront.tada'; + +export const GetCartOrderQuery = graphql(` + query GetCartOrder($id: ID!) { + orderById(id: $id) { + id + customerId + createdAt + updatedAt + context { + storeId + channelId + } + lineItems { + id + name + quantity + skuId + type + fulfillmentMode + details { + productAssetUrl + sku + unitOfMeasure + selectedOptions { + attribute + values + } + selectedAddons { + attribute + sku + values { + name + costAdjustment { + value + currencyCode + } + } + } + } + totals { + subTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + } + discounts { + id + name + code + amount { + value + currencyCode + } + ratePercentage + } + taxes { + id + name + amount { + value + currencyCode + } + ratePercentage + } + notes { + id + content + author + authorType + } + } + totals { + subTotal { + value + currencyCode + } + shippingTotal { + value + currencyCode + } + taxTotal { + value + currencyCode + } + discountTotal { + value + currencyCode + } + productDiscountTotal { + value + currencyCode + } + shippingDiscountTotal { + value + currencyCode + } + feeTotal { + value + currencyCode + } + total { + value + currencyCode + } + } + discounts { + id + name + code + amount { + value + currencyCode + } + ratePercentage + appliedBeforeTax + } + taxes { + id + name + amount { + value + currencyCode + } + ratePercentage + included + exempted + } + shipping { + firstName + lastName + email + phone + companyName + address { + addressLine1 + addressLine2 + addressLine3 + adminArea1 + adminArea2 + adminArea3 + adminArea4 + postalCode + countryCode + } + } + notes { + id + content + author + authorType + createdAt + } + tags + } + } +`); diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 7f78bda9..d4b06da6 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -23,6 +23,15 @@ import { DraftOrderSkusQuery, DraftOrderTaxesQuery, } from '@/lib/godaddy/checkout-queries.ts'; +import { + AddCartOrderMutation, + AddLineItemBySkuIdMutation, + ApplyDiscountCodesMutation, + DeleteLineItemByIdMutation, + UpdateCartOrderMutation, + UpdateLineItemByIdMutation, +} from '@/lib/godaddy/orders-storefront-mutations.ts'; +import { GetCartOrderQuery } from '@/lib/godaddy/orders-storefront-queries.ts'; export const PaymentProvider = { STRIPE: 'stripe', @@ -270,3 +279,68 @@ export type SKUAttributeValue = NonNullable< >['edges'] >[number] >['node']; + +// Cart/Orders types +export type AddCartOrderInput = VariablesOf< + typeof AddCartOrderMutation +>['input']; + +export type AddLineItemBySkuIdInput = VariablesOf< + typeof AddLineItemBySkuIdMutation +>['input']; + +export type UpdateCartOrderInput = VariablesOf< + typeof UpdateCartOrderMutation +>['input']; + +export type UpdateLineItemByIdInput = VariablesOf< + typeof UpdateLineItemByIdMutation +>['input']; + +export type DeleteLineItemByIdInput = VariablesOf< + typeof DeleteLineItemByIdMutation +>; + +export type ApplyDiscountCodesInput = VariablesOf< + typeof ApplyDiscountCodesMutation +>['input']; + +export type CartOrder = NonNullable< + ResultOf['orderById'] +>; + +export type CartLineItem = NonNullable< + NonNullable['orderById']>['lineItems'] +>[number]; + +export type CartOrderTotals = NonNullable< + NonNullable['orderById']>['totals'] +>; + +export type CartLineItemTotals = NonNullable< + NonNullable< + NonNullable['orderById']>['lineItems'] + >[number]['totals'] +>; + +export type CartDiscount = NonNullable< + NonNullable['orderById']>['discounts'] +>[number]; + +export type CartTax = NonNullable< + NonNullable['orderById']>['taxes'] +>[number]; + +export type CartShippingInfo = NonNullable< + NonNullable['orderById']>['shipping'] +>; + +export type CartNote = NonNullable< + NonNullable['orderById']>['notes'] +>[number]; + +export type CartLineItemDetails = NonNullable< + NonNullable< + NonNullable['orderById']>['lineItems'] + >[number]['details'] +>;