From 1c6f2dd653a569eabde84e60a2ae52a2096a39bd Mon Sep 17 00:00:00 2001 From: Patryk Smolarz Date: Thu, 11 Apr 2024 11:30:28 +0200 Subject: [PATCH 1/4] preparing to V% custom app [V% products, other localisations] --- composable-ui/.gitignore | 2 + .../src/components/cart/__data__/cart-data.ts | 9 +- .../cart/cart-drawer/cart-drawer.tsx | 13 +- .../src/components/cart/cart-item-data.tsx | 9 +- .../src/components/cart/cart-page.tsx | 13 +- .../src/components/checkout/products-list.tsx | 9 +- .../src/components/ev-kiosk-and-pomp-page.tsx | 227 ------------------ .../src/components/layout/header.tsx | 4 +- composable-ui/src/components/pos-page.tsx | 19 +- .../src/components/pos/products-list.tsx | 41 ++-- composable-ui/src/components/product-page.tsx | 48 ++-- composable-ui/src/hooks/use-cart.ts | 11 +- composable-ui/src/hooks/use-get-products.ts | 27 +++ composable-ui/src/hooks/use-localisation.ts | 13 +- composable-ui/src/hooks/use-pos-checkout.tsx | 10 +- composable-ui/src/pages/ev-kiosk-and-pomp.tsx | 27 --- composable-ui/src/pages/product/[slug].tsx | 5 +- .../commerce/procedures/cart/add-cart-item.ts | 37 ++- .../routers/commerce/procedures/cart/index.ts | 1 + .../commerce/procedures/cart/products-list.ts | 6 + .../procedures/catalog/get-product-by.ts | 4 +- .../src/data/generate-cart-data.ts | 42 ++-- .../src/services/cart/add-cart-item.ts | 9 +- .../src/services/cart/get-products-list.ts | 8 + .../src/services/cart/index.ts | 1 + .../src/services/catalog/get-product-by.ts | 9 +- packages/types/src/commerce/cart.ts | 9 +- .../types/src/commerce/commerce-service.ts | 34 ++- packages/ui/src/components/gallery.tsx | 162 ++++--------- packages/voucherify/src/get-product.ts | 12 + packages/voucherify/src/get-products.ts | 12 + packages/voucherify/src/index.ts | 2 + 32 files changed, 319 insertions(+), 516 deletions(-) delete mode 100644 composable-ui/src/components/ev-kiosk-and-pomp-page.tsx create mode 100644 composable-ui/src/hooks/use-get-products.ts delete mode 100644 composable-ui/src/pages/ev-kiosk-and-pomp.tsx create mode 100644 composable-ui/src/server/api/routers/commerce/procedures/cart/products-list.ts create mode 100644 packages/commerce-generic/src/services/cart/get-products-list.ts create mode 100644 packages/voucherify/src/get-product.ts create mode 100644 packages/voucherify/src/get-products.ts diff --git a/composable-ui/.gitignore b/composable-ui/.gitignore index bc1f7a7..85e41e1 100644 --- a/composable-ui/.gitignore +++ b/composable-ui/.gitignore @@ -1,2 +1,4 @@ public/sitemap* public/robots.txt + +.env*.local \ No newline at end of file diff --git a/composable-ui/src/components/cart/__data__/cart-data.ts b/composable-ui/src/components/cart/__data__/cart-data.ts index e17686a..14b5e59 100644 --- a/composable-ui/src/components/cart/__data__/cart-data.ts +++ b/composable-ui/src/components/cart/__data__/cart-data.ts @@ -6,18 +6,13 @@ export const cartData: CartData = { { id: '1', category: 'Accessories', - type: 'Bag', name: 'Venture Daypack', brand: 'Riley', price: 129, tax: 0.07, - image: { - url: '/img/products/_0000s_0001_backpack-rugged-black-front.jpg', - alt: '', - }, - sku: 'SKU-A1-2345', - slug: 'venture-daypack', + image_url: '/img/products/_0000s_0001_backpack-rugged-black-front.jpg', quantity: 1, + slug: 'venture-daypack', }, ], summary: { diff --git a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx index e93d329..f301254 100644 --- a/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx +++ b/composable-ui/src/components/cart/cart-drawer/cart-drawer.tsx @@ -102,15 +102,18 @@ export const CartDrawer = () => { columns={4} editable details={[ - { name: 'SKU', value: item.sku, id: item.id }, - { name: 'Type', value: item.type, id: item.id }, + { + name: 'Category', + value: item.category, + id: item.id, + }, ]} size={'sm'} image={{ - src: item.image.url, - alt: item.image.alt ?? item.name, + src: item.image_url || '', + alt: item.name || '', onClickImage: () => - router.push(`/product/${item.slug}`), + router.push(`/product/${item.slug}?id=${item.id}`), }} name={item.name || ''} labels={{ diff --git a/composable-ui/src/components/cart/cart-item-data.tsx b/composable-ui/src/components/cart/cart-item-data.tsx index 975c46c..271d140 100644 --- a/composable-ui/src/components/cart/cart-item-data.tsx +++ b/composable-ui/src/components/cart/cart-item-data.tsx @@ -15,7 +15,7 @@ export const CartItemData = ({ cartItem }: CartItemDataProps) => { @@ -28,8 +28,8 @@ export const CartItemData = ({ cartItem }: CartItemDataProps) => { > {cartItem.image.alt} { {cartItem.name} - - {intl.formatMessage({ id: 'product.sku' }, { sku: cartItem.sku })} - diff --git a/composable-ui/src/components/cart/cart-page.tsx b/composable-ui/src/components/cart/cart-page.tsx index 58f2d90..6570672 100644 --- a/composable-ui/src/components/cart/cart-page.tsx +++ b/composable-ui/src/components/cart/cart-page.tsx @@ -110,15 +110,18 @@ export const CartPage = () => { columns={4} editable details={[ - { name: 'SKU', value: item.sku, id: item.id }, - { name: 'Type', value: item.type, id: item.id }, + { + name: 'Category', + value: item.category, + id: item.id, + }, ]} size={productCartSize} image={{ - src: item.image.url, - alt: item.image.alt ?? item.name, + src: item.image_url || '', + alt: item.name || '', onClickImage: () => - router.push(`/product/${item.slug}`), + router.push(`/product/${item.slug}?&id=${item.id}`), }} metaText={ productCartSize === 'lg' diff --git a/composable-ui/src/components/checkout/products-list.tsx b/composable-ui/src/components/checkout/products-list.tsx index f9507d8..41960ee 100644 --- a/composable-ui/src/components/checkout/products-list.tsx +++ b/composable-ui/src/components/checkout/products-list.tsx @@ -32,13 +32,14 @@ export const ProductsList = ({ key={`${item.name}-${item.id}`} columns={2} size="lg" - name={item.name} + name={item.name || item.id} brand={item.brand} quantity={item.quantity} image={{ - src: item.image.url, - alt: item.image.alt ?? item.name, - onClickImage: () => router.push(`/product/${item.slug}`), + src: item.image_url || '', + alt: item.name || '', + onClickImage: () => + router.push(`/product/${item.slug}?id=${item.id}`), }} regularPrice={intl.formatNumber(item.price, currencyFormatConfig)} labels={{ diff --git a/composable-ui/src/components/ev-kiosk-and-pomp-page.tsx b/composable-ui/src/components/ev-kiosk-and-pomp-page.tsx deleted file mode 100644 index 1bd74d4..0000000 --- a/composable-ui/src/components/ev-kiosk-and-pomp-page.tsx +++ /dev/null @@ -1,227 +0,0 @@ -import { FormatNumberOptions, useIntl } from 'react-intl' -import { - Alert, - AlertDescription, - AlertIcon, - AlertTitle, - Box, - Button, - Container, - Flex, - Grid, - GridItem, - Text, - useBreakpointValue, -} from '@chakra-ui/react' -import { signOut } from 'next-auth/react' -import products from '@composable/commerce-generic/src/data/products.json' - -import { APP_CONFIG } from '../utils/constants' -import { useCart, usePosCheckout, useToast } from 'hooks' -import { HorizontalProductCardEditablePomp } from '@composable/ui' -import { CartSummaryPomp } from './cart' -import { ProductsList } from './pos/products-list' -import { Customer } from './pos/customer' -import { CustomerRedeemable } from './pos/customer-redeemables' -import { useEffect, useState } from 'react' -import { LoyaltyCardsList } from './pos/loyalty-cards-list' -import { Order } from '@composable/types' -import { useRouter } from 'next/router' - -type PompCartItems = { - name: string - id: string - sku: string - type: string - quantity: number - price: number - imageUrl: string - imageAlt: string -} - -type PompCart = { - items: PompCartItems[] - taxes: number - totalPrice: number - subtotal: number -} - -export const EvKioskAndPompPage = () => { - const intl = useIntl() - - const [orderAdded, setOrderAdded] = useState() - const toast = useToast() - const router = useRouter() - - const productsList = products.filter((product) => - ['EV Charging 1 kWh', 'Gasoline'].includes(product.name) - ) - - const [pompCart, setPompCart] = useState({ - taxes: 0, - totalPrice: 0, - subtotal: 0, - items: productsList.map((product) => ({ - name: product.name, - id: product.id, - sku: product.sku, - type: product.type, - price: product.price, - quantity: 0, - imageUrl: product.images[0].url, - imageAlt: product.name, - })), - }) - - const { placeOrder, order } = usePosCheckout({ - onPlaceOrderSuccess: async (order: Order | undefined) => { - if (order) { - await signOut({ redirect: false }) - router.push(`/order/${order.voucherifyOrderId}`) - } else { - setOrderAdded(order) - } - }, - }) - - const productCartSize: 'sm' | 'lg' | undefined = useBreakpointValue({ - base: 'sm', - md: 'lg', - }) - const currencyFormatConfig: FormatNumberOptions = { - currency: APP_CONFIG.CURRENCY_CODE, - style: 'currency', - } - - const paidByCash = async () => { - await placeOrder( - pompCart.items.map((item) => ({ - productId: item.id, - quantity: item.quantity, - })) - ) - } - - return ( - - - - - - - - Order - - - - <> - - {pompCart.items?.map((item) => { - return ( - - { - setPompCart((cart) => { - const newItems = cart.items.map((cItem) => { - if (cItem.id === item.id) { - return { - ...cItem, - quantity: val, - } - } - return cItem - }) - - const subtotal = newItems.reduce( - (prev, current) => - prev + current.price * current.quantity, - 0 - ) - const taxes = subtotal * 0.07 - - return { - ...cart, - items: newItems, - taxes, - subtotal, - totalPrice: taxes + subtotal, - } - }) - console.log({ - quantity: val, - itemId: item.id, - }) - }} - /> - - ) - })} - - - {/*
{JSON.stringify(pompCart, null, 2)}
*/} - -
- -
-
- ) -} diff --git a/composable-ui/src/components/layout/header.tsx b/composable-ui/src/components/layout/header.tsx index 8d04010..bbba984 100644 --- a/composable-ui/src/components/layout/header.tsx +++ b/composable-ui/src/components/layout/header.tsx @@ -79,14 +79,14 @@ export const Header = () => { height: 'full', }} /> - + /> */} { @@ -71,13 +71,13 @@ export const PosPage = () => { style: 'currency', } - const handleAddToCart = (productId: unknown) => { - if (!productId || typeof productId !== 'string') { + const handleAddToCart = (product: ProductListResponse) => { + if (!product.id || typeof product.id !== 'string') { return } addCartItem.mutate({ - productId: productId, + product, quantity: 1, }) } @@ -165,13 +165,16 @@ export const PosPage = () => { brand={item.brand} columns={4} details={[ - { name: 'SKU', value: item.sku, id: item.id }, - { name: 'Type', value: item.type, id: item.id }, + { + name: 'Category', + value: item.category, + id: item.id, + }, ]} size={productCartSize} image={{ - src: item.image.url, - alt: item.image.alt ?? item.name, + src: item.image_url || '', + alt: item.name || '', }} name={item.name || ''} labels={{ diff --git a/composable-ui/src/components/pos/products-list.tsx b/composable-ui/src/components/pos/products-list.tsx index 7c09b9f..99b6464 100644 --- a/composable-ui/src/components/pos/products-list.tsx +++ b/composable-ui/src/components/pos/products-list.tsx @@ -9,43 +9,35 @@ import { Text, Divider, } from '@chakra-ui/react' +import { useGetProductList } from 'hooks/use-get-products' +import { ProductListResponse } from '@composable/types' export interface ProductsProps { - onClick?: (productId: string) => unknown - filterProductsByName?: string[] - filterProductsOutByName?: string[] + onClick?: (product: ProductListResponse) => unknown } -export const ProductsList = ({ - onClick, - filterProductsByName, - filterProductsOutByName, -}: ProductsProps) => { - const produuctsToList = filterProductsByName - ? products.filter((product) => filterProductsByName.includes(product.name)) - : filterProductsOutByName - ? products.filter( - (product) => !filterProductsOutByName.includes(product.name) - ) - : products +export const ProductsList = ({ onClick }: ProductsProps) => { + const { productsList, status } = useGetProductList() + return ( - {produuctsToList - .sort((p1, p2) => p1.type.localeCompare(p2.type)) - .map((product) => ( + {productsList?.length === 0 ? ( +

no products

+ ) : ( + productsList?.map((product) => ( onClick?.(product.id)} - key={product.id} + onClick={() => onClick?.(product)} + key={product.name} > - {product.images[0] && ( + {product.image_url && ( {product.images[0]?.alt}{product.name} - ))} + )) + )}
) } diff --git a/composable-ui/src/components/product-page.tsx b/composable-ui/src/components/product-page.tsx index 0395c14..2213def 100644 --- a/composable-ui/src/components/product-page.tsx +++ b/composable-ui/src/components/product-page.tsx @@ -23,17 +23,22 @@ export const ProductPage = () => { const intl = useIntl() const toast = useToast() const { data: product, isLoading } = api.commerce.getProductBy.useQuery({ - slug: `${router.query.slug}`, + id: `${router.query.id}`, }) // TODO: breadcrumb data should come from product const breadcrumb = [ { href: '/', label: 'Home' }, { - href: `/category/${product?.category}`, - label: product?.category ?? 'Category', + href: `/category/${ + product?.metadata?.category || product?.metadata?.food_category || '' + }`, + label: + (product?.metadata?.category || + product?.metadata?.food_category || + '') ?? + 'Category', }, - { href: '/', label: product?.type ?? 'Type' }, ] const [quantity, setQuantity] = useState(1) const { addCartItem } = useCart({ @@ -63,7 +68,7 @@ export const ProductPage = () => { } addCartItem.mutate({ - productId: product.id, + product, quantity: quantity, }) } @@ -76,24 +81,15 @@ export const ProductPage = () => { return } - const phpAccordion = [ - { - defaultOpen: false, - label: 'Material & Care', - content: product.materialAndCare, - id: 'b3ac576d-c527-4818-9540-fbc3933b5fb7', - }, - ...pdpAccordionData, - ] + const phpAccordion = [...pdpAccordionData] return ( } - brand={product.brand} + seo={} breadcrumb={} - title={product.name} - description={product.description} + title={product.name || ''} isLoaded={!isLoading} + description={''} sectionOrder={[ 'breadcrumb', 'brand', @@ -111,7 +107,7 @@ export const ProductPage = () => { height: 'fit-content', top: '12', }} - price={} + price={} main={ <> { width: '100%', borderRadius: 'base', }} - images={ - product?.images?.length - ? product.images.map((image, i) => { - return { - src: image.url, - alt: image.alt, - priority: i === 0, - } - }) - : [{ src: APP_CONFIG.IMAGE_PLACEHOLDER, alt: '' }] + image={ + product?.image_url + ? product.image_url + : APP_CONFIG.IMAGE_PLACEHOLDER } /> } diff --git a/composable-ui/src/hooks/use-cart.ts b/composable-ui/src/hooks/use-cart.ts index 4c67f18..8ecfca8 100644 --- a/composable-ui/src/hooks/use-cart.ts +++ b/composable-ui/src/hooks/use-cart.ts @@ -10,7 +10,7 @@ import { LOCAL_STORAGE_CART_ID, LOCAL_STORAGE_CART_UPDATED_AT, } from 'utils/constants' -import { Cart } from '@composable/types' +import { Cart, ProductListResponse } from '@composable/types' import { useSession } from 'next-auth/react' import { useLocalisation } from './use-localisation' @@ -102,13 +102,13 @@ export const useCart = (options?: UseCartOptions) => { ['cartItemAdd'], async (variables: { cartId: string - productId: string + product: ProductListResponse variantId?: string quantity: number }) => { const params = { cartId: variables.cartId, - productId: variables.productId, + product: variables.product, variantId: variables.variantId, quantity: variables.quantity, localisation, @@ -136,7 +136,8 @@ export const useCart = (options?: UseCartOptions) => { */ const cartItemAddMutation = useCallback( async (params: { - productId: string + // productId: string + product: ProductListResponse variantId?: string quantity: number }) => { @@ -144,7 +145,7 @@ export const useCart = (options?: UseCartOptions) => { await cartItemAdd.mutate( { cartId: id, - productId: params.productId, + product: params.product, variantId: params.variantId, quantity: params.quantity, }, diff --git a/composable-ui/src/hooks/use-get-products.ts b/composable-ui/src/hooks/use-get-products.ts new file mode 100644 index 0000000..c1915f1 --- /dev/null +++ b/composable-ui/src/hooks/use-get-products.ts @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query' +import { useSession } from 'next-auth/react' +import { api } from 'utils/api' + +const USE_GET_PRODUCTS_LIST = 'useGetProductsListKey' + +export const useGetProductList = () => { + const session = useSession() + const { client } = api.useContext() + const { data: productsList, status } = useQuery( + [USE_GET_PRODUCTS_LIST], + async () => { + const response = await client.commerce.getProductsList.query() + return response + }, + { + enabled: session.status === 'authenticated', + retry: false, + keepPreviousData: true, + } + ) + + return { + status, + productsList, + } +} diff --git a/composable-ui/src/hooks/use-localisation.ts b/composable-ui/src/hooks/use-localisation.ts index d8b9546..b0c926e 100644 --- a/composable-ui/src/hooks/use-localisation.ts +++ b/composable-ui/src/hooks/use-localisation.ts @@ -7,13 +7,12 @@ import { const LOCAL_STORAGE_LOCALISATION = 'pos_localisation_id' export const LOCALISATIONS = [ - 'West Parkland', - 'Fas Gas', - 'Parkland Calgary', - 'Husky Market', - 'Petro Canada Toronto', - 'Esso Vancouver', - 'Ultramar Montreal', + 'New York', + 'London', + 'Paris', + 'Warsaw', + 'Vancouver', + 'Lima', ] export const useLocalisation = () => { diff --git a/composable-ui/src/hooks/use-pos-checkout.tsx b/composable-ui/src/hooks/use-pos-checkout.tsx index 0a9958e..a4a0708 100644 --- a/composable-ui/src/hooks/use-pos-checkout.tsx +++ b/composable-ui/src/hooks/use-pos-checkout.tsx @@ -5,7 +5,7 @@ import { api } from 'utils/api' import { CheckoutContext } from 'components/checkout/checkout-provider' import { useCart } from './use-cart' import { PAYMENT_METHOD } from 'components/checkout/constants' -import { Order } from '@composable/types' +import { Order, CartItem, ProductListResponse } from '@composable/types' import { useLocalisation } from './use-localisation' interface UsePosCheckoutOptions { @@ -31,7 +31,9 @@ export const usePosCheckout = (options?: UsePosCheckoutOptions) => { */ const placeOrderMutation = useMutation( ['cartCheckout'], - async (variables: { items?: Item[] }) => { + async (variables: { + items?: (ProductListResponse & { quantity: number })[] + }) => { let cartId if (variables.items) { const response = await client.commerce.createCart.mutate() @@ -42,7 +44,7 @@ export const usePosCheckout = (options?: UsePosCheckoutOptions) => { await client.commerce.addCartItem.mutate({ localisation, cartId: cartId, - productId: item.productId, + product: item, quantity: Number(item.quantity), }) } @@ -94,7 +96,7 @@ export const usePosCheckout = (options?: UsePosCheckoutOptions) => { ) const placeOrder = useCallback( - (items?: Item[]) => { + (items?: (ProductListResponse & { quantity: number })[]) => { return placeOrderMutation.mutateAsync({ items }) }, [placeOrderMutation] diff --git a/composable-ui/src/pages/ev-kiosk-and-pomp.tsx b/composable-ui/src/pages/ev-kiosk-and-pomp.tsx deleted file mode 100644 index c8b8a37..0000000 --- a/composable-ui/src/pages/ev-kiosk-and-pomp.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { GetStaticProps } from 'next' -import { createServerApp } from 'server/isr/server-app' -import { EvKioskAndPompPage } from 'components/ev-kiosk-and-pomp-page' - -export const getStaticProps: GetStaticProps = async (context) => { - // In this particular implementation, the application is using "Static Site Generation" (SSG) - // to generate pages on the server, which will be served as pre-built HTML files to the client. - // The purpose of this is to provide fast initial loading times for the user, - // as well as to reduce the load on the server. - - const { ssg } = await createServerApp({ context }) - await ssg.cms.getPage.prefetch({ slug: 'home' }) - - return { - props: { - trpcState: ssg.dehydrate(), - }, - // The cache control mechanism will be relied on to regenerate the page - // using "Incremental Static Regeneration" (ISR). - // This is to prevent flashing of content - if the page was refetched every - // time the user visited it, they would likely see a brief flash of an older - // version of the page before the updated content is loaded. - revalidate: 60, - } -} - -export default EvKioskAndPompPage diff --git a/composable-ui/src/pages/product/[slug].tsx b/composable-ui/src/pages/product/[slug].tsx index c929bc3..3ba8790 100644 --- a/composable-ui/src/pages/product/[slug].tsx +++ b/composable-ui/src/pages/product/[slug].tsx @@ -22,8 +22,9 @@ export const getStaticPaths: GetStaticPaths = async (context) => { export const getStaticProps: GetStaticProps = async (context) => { const { ssg } = await createServerApp({ context }) - const slug = `${context?.params?.slug?.toString()}` - await ssg.commerce.getProductBy.prefetch({ slug }) + // const slug = `${context?.params?.slug?.toString()}` + const productId = `${context?.params?.id?.toString()}` + await ssg.commerce.getProductBy.prefetch({ id: productId }) return { props: { diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/add-cart-item.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/add-cart-item.ts index 1ed2fa8..e683f57 100644 --- a/composable-ui/src/server/api/routers/commerce/procedures/cart/add-cart-item.ts +++ b/composable-ui/src/server/api/routers/commerce/procedures/cart/add-cart-item.ts @@ -1,12 +1,47 @@ import { z } from 'zod' import { protectedProcedure } from 'server/api/trpc' import { commerce } from 'server/data-source' +import { ProductListResponse } from '@composable/types' + +const ProductListSchema = z.object({ + id: z.string(), + source_id: z.string().optional(), + object: z.literal('product'), + name: z.string().optional(), + price: z.number().optional(), + attributes: z.array(z.string()).optional(), + created_at: z.string(), + image_url: z.string().nullable().optional(), + metadata: z.record(z.any()).optional(), + skus: z + .object({ + object: z.literal('list'), + total: z.number(), + data: z + .array( + z.object({ + id: z.string(), + source_id: z.string().optional(), + sku: z.string().optional(), + price: z.number().optional(), + attributes: z.record(z.string()).optional(), + metadata: z.record(z.any()).optional(), + updated_at: z.string().optional(), + currency: z.string().optional(), + created_at: z.string(), + object: z.literal('sku'), + }) + ) + .optional(), + }) + .optional(), +}) satisfies z.ZodSchema export const addCartItem = protectedProcedure .input( z.object({ cartId: z.string(), - productId: z.string(), + product: ProductListSchema, variantId: z.string().optional(), quantity: z.number(), localisation: z.string(), diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts index b90b6ce..33ffc8d 100644 --- a/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts +++ b/composable-ui/src/server/api/routers/commerce/procedures/cart/index.ts @@ -8,3 +8,4 @@ export * from './delete-voucher' export * from './loyalty-cards-list' export * from './orders-list' export * from './customer-redeemables' +export * from './products-list' diff --git a/composable-ui/src/server/api/routers/commerce/procedures/cart/products-list.ts b/composable-ui/src/server/api/routers/commerce/procedures/cart/products-list.ts new file mode 100644 index 0000000..d9c212e --- /dev/null +++ b/composable-ui/src/server/api/routers/commerce/procedures/cart/products-list.ts @@ -0,0 +1,6 @@ +import { protectedProcedure } from 'server/api/trpc' +import { commerce } from 'server/data-source' + +export const getProductsList = protectedProcedure.query(async () => { + return await commerce.getProductsList() +}) diff --git a/composable-ui/src/server/api/routers/commerce/procedures/catalog/get-product-by.ts b/composable-ui/src/server/api/routers/commerce/procedures/catalog/get-product-by.ts index 17427e0..77d7b86 100644 --- a/composable-ui/src/server/api/routers/commerce/procedures/catalog/get-product-by.ts +++ b/composable-ui/src/server/api/routers/commerce/procedures/catalog/get-product-by.ts @@ -3,7 +3,7 @@ import { publicProcedure } from 'server/api/trpc' import { commerce } from 'server/data-source' export const getProductBy = publicProcedure - .input(z.object({ slug: z.string() })) + .input(z.object({ id: z.string() })) .query(async ({ input }) => { - return await commerce.getProductBy({ slug: input.slug }) + return await commerce.getProductBy({ id: input.id }) }) diff --git a/packages/commerce-generic/src/data/generate-cart-data.ts b/packages/commerce-generic/src/data/generate-cart-data.ts index 4a389d0..e159e2f 100644 --- a/packages/commerce-generic/src/data/generate-cart-data.ts +++ b/packages/commerce-generic/src/data/generate-cart-data.ts @@ -1,11 +1,5 @@ -import { Cart, CartItem } from '@composable/types' -import products from './products.json' +import { Cart, CartItem, ProductListResponse } from '@composable/types' import { randomUUID } from 'crypto' - -const findProductById = (id: string) => { - return products.find((product) => product.id === id) ?? products[0] -} - export const generateEmptyCart = (cartId?: string): Cart => ({ id: cartId || randomUUID(), items: [], @@ -14,22 +8,24 @@ export const generateEmptyCart = (cartId?: string): Cart => ({ summary: {}, }) -export const generateCartItem = (productId: string, quantity: number) => { - const _product = findProductById(productId) - return { - brand: _product.brand, - category: _product.category, - id: _product.id, - image: _product.images[0], - name: _product.name, - price: _product.price, - tax: _product.price * 0.07, - quantity: quantity ?? 1, - sku: _product.sku, - slug: _product.slug, - type: _product.type, - } -} +export const generateCartItem = ( + product: ProductListResponse, + quantity: number +) => ({ + id: product.id, + category: product.metadata?.food_category || product.metadata?.category || '', + brand: product.metadata?.brand, + image_url: product.image_url || null, + name: product.name || null, + price: product.price ? product.price / 100 : 0, + tax: product.price ? product.price * 0.07 : 0, + quantity: quantity ?? 1, + slug: + product.name + ?.toLowerCase() + .replace(/[^\w\s]/gi, '') + .replace(' ', '-') || product.id, +}) export const calculateCartSummary = ( cartItems: CartItem[] diff --git a/packages/commerce-generic/src/services/cart/add-cart-item.ts b/packages/commerce-generic/src/services/cart/add-cart-item.ts index 03b6e9c..e8d9035 100644 --- a/packages/commerce-generic/src/services/cart/add-cart-item.ts +++ b/packages/commerce-generic/src/services/cart/add-cart-item.ts @@ -9,21 +9,20 @@ import { updateCartDiscount } from '@composable/voucherify' export const addCartItem: CommerceService['addCartItem'] = async ({ cartId, - productId, + product, quantity, user, localisation, }) => { const cart = (await getCart(cartId)) || generateEmptyCart(cartId) - const isProductInCartAlready = cart.items.some( - (item) => item.id === productId + (item) => item.id === product.id ) if (isProductInCartAlready) { - cart.items.find((item) => item.id === productId)!.quantity++ + cart.items.find((item) => item.id === product.id)!.quantity++ } else { - const newItem = generateCartItem(productId, quantity) + const newItem = generateCartItem(product, quantity) cart.items.push(newItem) } diff --git a/packages/commerce-generic/src/services/cart/get-products-list.ts b/packages/commerce-generic/src/services/cart/get-products-list.ts new file mode 100644 index 0000000..eb41bb8 --- /dev/null +++ b/packages/commerce-generic/src/services/cart/get-products-list.ts @@ -0,0 +1,8 @@ +import { CommerceService } from '@composable/types' +import { getProducts } from '@composable/voucherify' + +export const getProductsList: CommerceService['getProductsList'] = async () => { + const productsList = await getProducts() + + return productsList +} diff --git a/packages/commerce-generic/src/services/cart/index.ts b/packages/commerce-generic/src/services/cart/index.ts index 243ddef..c764d49 100644 --- a/packages/commerce-generic/src/services/cart/index.ts +++ b/packages/commerce-generic/src/services/cart/index.ts @@ -9,3 +9,4 @@ export * from './to-cent' export * from './get-loyalty-cards-list' export * from './get-orders-list' export * from './get-customer-redeemables' +export * from './get-products-list' diff --git a/packages/commerce-generic/src/services/catalog/get-product-by.ts b/packages/commerce-generic/src/services/catalog/get-product-by.ts index 3faeb16..0d4373d 100644 --- a/packages/commerce-generic/src/services/catalog/get-product-by.ts +++ b/packages/commerce-generic/src/services/catalog/get-product-by.ts @@ -1,8 +1,7 @@ import { CommerceService } from '@composable/types' -import products from '../../data/products.json' +import { getProduct } from '@composable/voucherify' -export const getProductBy: CommerceService['getProductBy'] = async ({ - slug, -}) => { - return products.find((el) => el.slug === slug) ?? null +export const getProductBy: CommerceService['getProductBy'] = async ({ id }) => { + const product = await getProduct(id) + return product } diff --git a/packages/types/src/commerce/cart.ts b/packages/types/src/commerce/cart.ts index 5225f30..5397686 100644 --- a/packages/types/src/commerce/cart.ts +++ b/packages/types/src/commerce/cart.ts @@ -32,13 +32,12 @@ export interface Voucher { export interface CartItem { id: string category: string - type: string - brand: string - image: { url: string; alt: string } - name: string + brand?: string | undefined + sku?: string | undefined + image_url: string | null + name: string | null price: number tax: number quantity: number - sku: string slug: string } diff --git a/packages/types/src/commerce/commerce-service.ts b/packages/types/src/commerce/commerce-service.ts index 6bddcc5..c1edf17 100644 --- a/packages/types/src/commerce/commerce-service.ts +++ b/packages/types/src/commerce/commerce-service.ts @@ -115,6 +115,34 @@ type Redeemable = { loyalty_card?: unknown } } + +export type ProductListResponse = { + id: string + source_id?: string + object: 'product' + name?: string + price?: number + attributes?: string[] + created_at: string + image_url?: string | null + metadata?: Record + skus?: { + object: 'list' + total: number + data?: { + id: string + source_id?: string + sku?: string + price?: number + attributes?: Record + metadata?: Record + updated_at?: string + currency?: string + created_at: string + object: 'sku' + }[] + } +} export interface CommerceService { /** * Cart methods @@ -122,7 +150,7 @@ export interface CommerceService { addCartItem(params: { cartId: string - productId: string + product: ProductListResponse variantId?: string quantity: number user?: UserSession @@ -182,7 +210,7 @@ export interface CommerceService { siteUrl: string }): Promise - getProductBy(params: { slug: string }): Promise + getProductBy(params: { id: string }): Promise getProductSitemap: (params: { siteUrl: string @@ -239,4 +267,6 @@ export interface CommerceService { cartId: string localisation?: string }): Promise + + getProductsList(): Promise } diff --git a/packages/ui/src/components/gallery.tsx b/packages/ui/src/components/gallery.tsx index 3774903..c124437 100644 --- a/packages/ui/src/components/gallery.tsx +++ b/packages/ui/src/components/gallery.tsx @@ -1,36 +1,27 @@ -import { useState } from 'react' import Image from 'next/image' import NextLink from 'next/link' import { AspectRatio, Box, - IconButton, - IconButtonProps, Link, Skeleton, Stack, StackProps, } from '@chakra-ui/react' -import { IoChevronBackOutline, IoChevronForwardOutline } from 'react-icons/io5' -import { Carousel, CarouselSlide, useCarousel } from './carousel' +import { Carousel, CarouselSlide } from './carousel' interface GalleryProps { aspectRatio?: number href?: string - images?: Array<{ src: string; alt: string; priority?: boolean }> + image?: string | '/img/image-placeholder.svg' productName?: String rootProps?: StackProps } export const Gallery = (props: GalleryProps) => { - const { images, aspectRatio = 3 / 4, rootProps, productName = '' } = props - const [currentSlide, setCurrentSlide] = useState(0) + const { image, aspectRatio = 3 / 4, rootProps, productName = '' } = props - const [ref, slider] = useCarousel({ - slideChanged: (slider) => setCurrentSlide(slider.track.details.rel), - }) - - if (!images) { + if (!image) { return ( @@ -42,9 +33,6 @@ export const Gallery = (props: GalleryProps) => { ) } - const hasPrevious = currentSlide !== 0 - const hasNext = currentSlide < images.length - 1 - return ( { direction={{ base: 'column-reverse', md: 'row' }} border={'none'} > - {images.length > 1 && ( + {image && ( - {images.map((image, i) => ( - slider.current?.moveToIdx(i)} - border={currentSlide === i ? '2px solid' : 'none'} - borderColor={'text'} - borderRadius={'8px'} - width={'64px'} - height={'64px'} + + - - {`View - - - ))} + {'Product + + )} { }, }} > - - {images.map((image, i) => ( - - - + + + + {`View - {`View - - - - ))} + /> + + + - {hasPrevious && ( - slider.current?.prev()} - icon={} - aria-label="Previous Slide" - /> - )} - - {hasNext && ( - slider.current?.next()} - icon={} - aria-label="Next Slide" - /> - )} ) } -const CarouselIconButton = (props: IconButtonProps) => ( - -) - const ElementLinkHandler = (props: { children: JSX.Element href?: string diff --git a/packages/voucherify/src/get-product.ts b/packages/voucherify/src/get-product.ts new file mode 100644 index 0000000..a7239fb --- /dev/null +++ b/packages/voucherify/src/get-product.ts @@ -0,0 +1,12 @@ +import { getVoucherify } from './voucherify-config' + +export const getProduct = async (productId: string) => { + try { + const voucherify = getVoucherify() + + const product = await voucherify.products.get(productId) + return product + } catch (err) { + return null + } +} diff --git a/packages/voucherify/src/get-products.ts b/packages/voucherify/src/get-products.ts new file mode 100644 index 0000000..d364f07 --- /dev/null +++ b/packages/voucherify/src/get-products.ts @@ -0,0 +1,12 @@ +import { getVoucherify } from './voucherify-config' + +export const getProducts = async () => { + try { + const voucherify = getVoucherify() + + const { products } = await voucherify.products.list() + return products + } catch (err) { + return [] + } +} diff --git a/packages/voucherify/src/index.ts b/packages/voucherify/src/index.ts index 5e8a3b0..2e8c93e 100644 --- a/packages/voucherify/src/index.ts +++ b/packages/voucherify/src/index.ts @@ -1 +1,3 @@ export * from './discount' +export * from './get-products' +export * from './get-product' From 95527879a49225ae09052198b42c0037b8056ed5 Mon Sep 17 00:00:00 2001 From: Patryk Smolarz Date: Fri, 12 Apr 2024 14:31:17 +0200 Subject: [PATCH 2/4] target esnext --- composable-ui/tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composable-ui/tsconfig.json b/composable-ui/tsconfig.json index b8247fa..41d4f59 100644 --- a/composable-ui/tsconfig.json +++ b/composable-ui/tsconfig.json @@ -3,6 +3,7 @@ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"], "compilerOptions": { - "baseUrl": "./src" + "baseUrl": "./src", + "target": "ESNext" } } From 99ecbd93d25f594b797fea0c0806c44189465e98 Mon Sep 17 00:00:00 2001 From: Patryk Smolarz Date: Fri, 12 Apr 2024 14:31:27 +0200 Subject: [PATCH 3/4] a lot of new domains (in the future to change and avoid kind of this situation) --- composable-ui/next.config.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/composable-ui/next.config.js b/composable-ui/next.config.js index 0ca4c29..617f404 100644 --- a/composable-ui/next.config.js +++ b/composable-ui/next.config.js @@ -45,6 +45,23 @@ module.exports = () => { 'voucherify-uploads.s3.amazonaws.com', 'voucherify.io', 'dev.dl.voucherify.io', + 'www.drinksupermarket.com', + 'www.nestlepurezavital.com.ar', + 'www.benjerry.com', + 'a.allegroimg.com', + 'i5.walmartimages.com', + 'www.teyli.eu', + 'basketo.pl', + 'asset1.cxnmarksandspencer.com', + 'japoniacentralna.pl', + 'content.woolovers.com', + 'encrypted-tbn0.gstatic.com', + 'm.media-amazon.com', + 'i.insider.com', + 'lsco.scene7.com', + 'media.boohoo.com', + 'pl.benetton.com', + 'eu.wrangler.com', ], formats: ['image/avif', 'image/webp'], minimumCacheTTL: 60 * 60 * 24 * 30, From 1879a962224fc870ec7dd7ccb36cb490f375393f Mon Sep 17 00:00:00 2001 From: Patryk Smolarz Date: Fri, 12 Apr 2024 14:33:34 +0200 Subject: [PATCH 4/4] product categories/localisations --- .../src/components/pos/products-list.tsx | 105 ++++++++++++------ composable-ui/src/components/pos/receipt.tsx | 12 +- .../src/services/cart/get-products-list.ts | 2 +- packages/voucherify/src/discount.ts | 3 +- .../src/order-to-voucherify-order.ts | 15 ++- 5 files changed, 88 insertions(+), 49 deletions(-) diff --git a/composable-ui/src/components/pos/products-list.tsx b/composable-ui/src/components/pos/products-list.tsx index 99b6464..2406050 100644 --- a/composable-ui/src/components/pos/products-list.tsx +++ b/composable-ui/src/components/pos/products-list.tsx @@ -1,4 +1,4 @@ -import { Box, Heading, SimpleGrid, Stack } from '@chakra-ui/react' +import { Box, Flex, Heading, SimpleGrid, Stack } from '@chakra-ui/react' import Image from 'next/image' import products from '@composable/commerce-generic/src/data/products.json' import { @@ -8,9 +8,11 @@ import { CardFooter, Text, Divider, + Select, } from '@chakra-ui/react' import { useGetProductList } from 'hooks/use-get-products' import { ProductListResponse } from '@composable/types' +import { ChangeEvent, ChangeEventHandler, useEffect, useState } from 'react' export interface ProductsProps { onClick?: (product: ProductListResponse) => unknown @@ -18,37 +20,78 @@ export interface ProductsProps { export const ProductsList = ({ onClick }: ProductsProps) => { const { productsList, status } = useGetProductList() + const [productsCategory, setProductsCategory] = useState('All') + + const productCategories = [ + ...new Set(productsList?.map((product) => product.metadata?.category)), + ] + + const handleOnChange = (e: ChangeEvent) => + setProductsCategory(e.target.value) + + const productsByCategory = (productCategory: string) => { + if (productCategory === 'All') { + return productsList + } + return productsList?.filter( + (product) => product.metadata?.category === productCategory + ) + } return ( - - {productsList?.length === 0 ? ( -

no products

- ) : ( - productsList?.map((product) => ( - onClick?.(product)} - key={product.name} - > - - {product.image_url && ( - {product.name - )} - {product.name} - - - )) - )} -
+ <> + + Choose category + + + + {products?.length === 0 ? ( +

No products

+ ) : ( + productsByCategory(productsCategory)?.map((product) => ( + onClick?.(product)} + key={product.name} + > + + {product.image_url && ( + {product.name + )} + {product.name} + + + )) + )} +
+ ) } diff --git a/composable-ui/src/components/pos/receipt.tsx b/composable-ui/src/components/pos/receipt.tsx index 9a9c2ce..701018f 100644 --- a/composable-ui/src/components/pos/receipt.tsx +++ b/composable-ui/src/components/pos/receipt.tsx @@ -50,13 +50,9 @@ export const Receipt = ({ order }: ReceiptProps) => { }) } - const locationId = - order.metadata && - order.metadata.location_id && - Array.isArray(order.metadata.location_id) && - order.metadata.location_id.length === 1 - ? order.metadata.location_id[0] - : 'Lorem ipsum' + const locationId = order?.metadata?.location_id + ? order.metadata?.location_id + : 'Lorem ipsum' return ( @@ -71,7 +67,7 @@ export const Receipt = ({ order }: ReceiptProps) => { - Address: {locationId} , 1234-5{' '} + Address: {<>{locationId}}, 1234-5 Tel: +1 012 345 67 89{' '} diff --git a/packages/commerce-generic/src/services/cart/get-products-list.ts b/packages/commerce-generic/src/services/cart/get-products-list.ts index eb41bb8..fdadf00 100644 --- a/packages/commerce-generic/src/services/cart/get-products-list.ts +++ b/packages/commerce-generic/src/services/cart/get-products-list.ts @@ -4,5 +4,5 @@ import { getProducts } from '@composable/voucherify' export const getProductsList: CommerceService['getProductsList'] = async () => { const productsList = await getProducts() - return productsList + return productsList?.filter((product) => product.metadata?.category) } diff --git a/packages/voucherify/src/discount.ts b/packages/voucherify/src/discount.ts index 3120a06..e69d4db 100644 --- a/packages/voucherify/src/discount.ts +++ b/packages/voucherify/src/discount.ts @@ -263,7 +263,7 @@ export const getOrdersList = async ( id: order.id, created_at: order.created_at, status: order.status, - location: order?.metadata?.location_id?.[0] || '', + location: order?.metadata?.location_id || '', amount: order.amount, })) } catch (e) { @@ -484,6 +484,7 @@ export const getCustomerRedeemables = async (props: { cartToVoucherifyOrder(cart), localisation ) + console.log(order, 'ORDER???') try { const voucherify = getVoucherify() const qualificationResponse = diff --git a/packages/voucherify/src/order-to-voucherify-order.ts b/packages/voucherify/src/order-to-voucherify-order.ts index 1ac22df..0f9af98 100644 --- a/packages/voucherify/src/order-to-voucherify-order.ts +++ b/packages/voucherify/src/order-to-voucherify-order.ts @@ -19,13 +19,12 @@ export const orderToVoucherifyOrder = (order: Order): OrdersCreate => { } } const LOCALISATIONS = [ - 'West Parkland', - 'Fas Gas', - 'Parkland Calgary', - 'Husky Market', - 'Petro Canada Toronto', - 'Esso Vancouver', - 'Ultramar Montreal', + 'New York', + 'London', + 'Paris', + 'Warsaw', + 'Vancouver', + 'Lima', ] export const addLocalisationToOrder = ( @@ -37,6 +36,6 @@ export const addLocalisationToOrder = ( } return { ...order, - metadata: { ...(order.metadata || {}), location_id: [localisation] }, + metadata: { ...(order?.metadata || {}), location_id: localisation }, } }