diff options
| author | Saumit <justsaumit@protonmail.com> | 2025-09-27 02:14:26 +0530 |
|---|---|---|
| committer | Saumit <justsaumit@protonmail.com> | 2025-09-27 02:14:26 +0530 |
| commit | 82e03978b89938219958032efb1448cc76baa181 (patch) | |
| tree | 626f3e54d52ecd49be0ed3bee30abacc0453d081 /src/frontend/pages | |
Initial snapshot - OpenTelemetry demo 2.1.3 -f
Diffstat (limited to 'src/frontend/pages')
| -rwxr-xr-x | src/frontend/pages/_app.tsx | 83 | ||||
| -rw-r--r-- | src/frontend/pages/_document.tsx | 65 | ||||
| -rwxr-xr-x | src/frontend/pages/api/cart.ts | 56 | ||||
| -rw-r--r-- | src/frontend/pages/api/checkout.ts | 44 | ||||
| -rw-r--r-- | src/frontend/pages/api/currency.ts | 25 | ||||
| -rw-r--r-- | src/frontend/pages/api/data.ts | 26 | ||||
| -rw-r--r-- | src/frontend/pages/api/products/[productId]/index.ts | 26 | ||||
| -rw-r--r-- | src/frontend/pages/api/products/index.ts | 26 | ||||
| -rw-r--r-- | src/frontend/pages/api/recommendations.ts | 33 | ||||
| -rw-r--r-- | src/frontend/pages/api/shipping.ts | 29 | ||||
| -rw-r--r-- | src/frontend/pages/cart/checkout/[orderId]/index.tsx | 61 | ||||
| -rw-r--r-- | src/frontend/pages/cart/index.tsx | 39 | ||||
| -rwxr-xr-x | src/frontend/pages/index.tsx | 48 | ||||
| -rw-r--r-- | src/frontend/pages/product/[productId]/index.tsx | 107 |
14 files changed, 668 insertions, 0 deletions
diff --git a/src/frontend/pages/_app.tsx b/src/frontend/pages/_app.tsx new file mode 100755 index 0000000..67ee8b1 --- /dev/null +++ b/src/frontend/pages/_app.tsx @@ -0,0 +1,83 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import '../styles/globals.css'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import App, { AppContext, AppProps } from 'next/app'; +import CurrencyProvider from '../providers/Currency.provider'; +import CartProvider from '../providers/Cart.provider'; +import { ThemeProvider } from 'styled-components'; +import Theme from '../styles/Theme'; +import FrontendTracer from '../utils/telemetry/FrontendTracer'; +import SessionGateway from '../gateways/Session.gateway'; +import { OpenFeatureProvider, OpenFeature } from '@openfeature/react-sdk'; +import { FlagdWebProvider } from '@openfeature/flagd-web-provider'; + +declare global { + interface Window { + ENV: { + NEXT_PUBLIC_PLATFORM?: string; + NEXT_PUBLIC_OTEL_SERVICE_NAME?: string; + NEXT_PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?: string; + IS_SYNTHETIC_REQUEST?: string; + }; + } +} + +if (typeof window !== 'undefined') { + FrontendTracer(); + if (window.location) { + const session = SessionGateway.getSession(); + + // Set context prior to provider init to avoid multiple http calls + OpenFeature.setContext({ targetingKey: session.userId, ...session }).then(() => { + /** + * We connect to flagd through the envoy proxy, straight from the browser, + * for this we need to know the current hostname and port. + */ + + const useTLS = window.location.protocol === 'https:'; + let port = useTLS ? 443 : 80; + if (window.location.port) { + port = parseInt(window.location.port, 10); + } + + OpenFeature.setProvider( + new FlagdWebProvider({ + host: window.location.hostname, + pathPrefix: 'flagservice', + port: port, + tls: useTLS, + maxRetries: 3, + maxDelay: 10000, + }) + ); + }); + } +} + +const queryClient = new QueryClient(); + +function MyApp({ Component, pageProps }: AppProps) { + return ( + <ThemeProvider theme={Theme}> + <OpenFeatureProvider> + <QueryClientProvider client={queryClient}> + <CurrencyProvider> + <CartProvider> + <Component {...pageProps} /> + </CartProvider> + </CurrencyProvider> + </QueryClientProvider> + </OpenFeatureProvider> + </ThemeProvider> + ); +} + +MyApp.getInitialProps = async (appContext: AppContext) => { + const appProps = await App.getInitialProps(appContext); + + return { ...appProps }; +}; + +export default MyApp; diff --git a/src/frontend/pages/_document.tsx b/src/frontend/pages/_document.tsx new file mode 100644 index 0000000..8a68bc0 --- /dev/null +++ b/src/frontend/pages/_document.tsx @@ -0,0 +1,65 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document'; +import { ServerStyleSheet } from 'styled-components'; +import {context, propagation} from "@opentelemetry/api"; + +const { ENV_PLATFORM, WEB_OTEL_SERVICE_NAME, PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, OTEL_COLLECTOR_HOST} = process.env; + +export default class MyDocument extends Document<{ envString: string }> { + static async getInitialProps(ctx: DocumentContext) { + const sheet = new ServerStyleSheet(); + const originalRenderPage = ctx.renderPage; + + try { + ctx.renderPage = () => + originalRenderPage({ + enhanceApp: App => props => sheet.collectStyles(<App {...props} />), + }); + + const initialProps = await Document.getInitialProps(ctx); + const baggage = propagation.getBaggage(context.active()); + const isSyntheticRequest = baggage?.getEntry('synthetic_request')?.value === 'true'; + + const otlpTracesEndpoint = isSyntheticRequest + ? `http://${OTEL_COLLECTOR_HOST}:4318/v1/traces` + : PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT; + + const envString = ` + window.ENV = { + NEXT_PUBLIC_PLATFORM: '${ENV_PLATFORM}', + NEXT_PUBLIC_OTEL_SERVICE_NAME: '${WEB_OTEL_SERVICE_NAME}', + NEXT_PUBLIC_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: '${otlpTracesEndpoint}', + IS_SYNTHETIC_REQUEST: '${isSyntheticRequest}', + };`; + return { + ...initialProps, + styles: [initialProps.styles, sheet.getStyleElement()], + envString, + }; + } finally { + sheet.seal(); + } + } + + render() { + return ( + <Html> + <Head> + <link rel="preconnect" href="https://fonts.googleapis.com" /> + <link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" /> + <link + href="https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;0,800;1,300;1,400;1,500;1,600;1,700;1,800&display=swap" + rel="stylesheet" + /> + </Head> + <body> + <Main /> + <script dangerouslySetInnerHTML={{ __html: this.props.envString }}></script> + <NextScript /> + </body> + </Html> + ); + } +} diff --git a/src/frontend/pages/api/cart.ts b/src/frontend/pages/api/cart.ts new file mode 100755 index 0000000..3f5b1b7 --- /dev/null +++ b/src/frontend/pages/api/cart.ts @@ -0,0 +1,56 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiHandler } from 'next'; +import CartGateway from '../../gateways/rpc/Cart.gateway'; +import { AddItemRequest, Empty } from '../../protos/demo'; +import ProductCatalogService from '../../services/ProductCatalog.service'; +import { IProductCart, IProductCartItem } from '../../types/Cart'; +import InstrumentationMiddleware from '../../utils/telemetry/InstrumentationMiddleware'; + +type TResponse = IProductCart | Empty; + +const handler: NextApiHandler<TResponse> = async ({ method, body, query }, res) => { + switch (method) { + case 'GET': { + const { sessionId = '', currencyCode = '' } = query; + const { userId, items } = await CartGateway.getCart(sessionId as string); + + const productList: IProductCartItem[] = await Promise.all( + items.map(async ({ productId, quantity }) => { + const product = await ProductCatalogService.getProduct(productId, currencyCode as string); + + return { + productId, + quantity, + product, + }; + }) + ); + + return res.status(200).json({ userId, items: productList }); + } + + case 'POST': { + const { userId, item } = body as AddItemRequest; + + await CartGateway.addItem(userId, item!); + const cart = await CartGateway.getCart(userId); + + return res.status(200).json(cart); + } + + case 'DELETE': { + const { userId } = body as AddItemRequest; + await CartGateway.emptyCart(userId); + + return res.status(204).send(''); + } + + default: { + return res.status(405); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/api/checkout.ts b/src/frontend/pages/api/checkout.ts new file mode 100644 index 0000000..6007ba2 --- /dev/null +++ b/src/frontend/pages/api/checkout.ts @@ -0,0 +1,44 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiRequest, NextApiResponse } from 'next'; +import InstrumentationMiddleware from '../../utils/telemetry/InstrumentationMiddleware'; +import CheckoutGateway from '../../gateways/rpc/Checkout.gateway'; +import { Empty, PlaceOrderRequest } from '../../protos/demo'; +import { IProductCheckoutItem, IProductCheckout } from '../../types/Cart'; +import ProductCatalogService from '../../services/ProductCatalog.service'; + +type TResponse = IProductCheckout | Empty; + +const handler = async ({ method, body, query }: NextApiRequest, res: NextApiResponse<TResponse>) => { + switch (method) { + case 'POST': { + const { currencyCode = '' } = query; + const orderData = body as PlaceOrderRequest; + const { order: { items = [], ...order } = {} } = await CheckoutGateway.placeOrder(orderData); + + const productList: IProductCheckoutItem[] = await Promise.all( + items.map(async ({ item: { productId = '', quantity = 0 } = {}, cost }) => { + const product = await ProductCatalogService.getProduct(productId, currencyCode as string); + + return { + cost, + item: { + productId, + quantity, + product, + }, + }; + }) + ); + + return res.status(200).json({ ...order, items: productList }); + } + + default: { + return res.status(405).send(''); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/api/currency.ts b/src/frontend/pages/api/currency.ts new file mode 100644 index 0000000..fd69909 --- /dev/null +++ b/src/frontend/pages/api/currency.ts @@ -0,0 +1,25 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiRequest, NextApiResponse } from 'next'; +import InstrumentationMiddleware from '../../utils/telemetry/InstrumentationMiddleware'; +import CurrencyGateway from '../../gateways/rpc/Currency.gateway'; +import { Empty } from '../../protos/demo'; + +type TResponse = string[] | Empty; + +const handler = async ({ method }: NextApiRequest, res: NextApiResponse<TResponse>) => { + switch (method) { + case 'GET': { + const { currencyCodes = [] } = await CurrencyGateway.getSupportedCurrencies(); + + return res.status(200).json(currencyCodes); + } + + default: { + return res.status(405); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/api/data.ts b/src/frontend/pages/api/data.ts new file mode 100644 index 0000000..7e6ac8f --- /dev/null +++ b/src/frontend/pages/api/data.ts @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiRequest, NextApiResponse } from 'next'; +import InstrumentationMiddleware from '../../utils/telemetry/InstrumentationMiddleware'; +import AdGateway from '../../gateways/rpc/Ad.gateway'; +import { Ad, Empty } from '../../protos/demo'; + +type TResponse = Ad[] | Empty; + +const handler = async ({ method, query }: NextApiRequest, res: NextApiResponse<TResponse>) => { + switch (method) { + case 'GET': { + const { contextKeys = [] } = query; + const { ads: adList } = await AdGateway.listAds(Array.isArray(contextKeys) ? contextKeys : contextKeys.split(',')); + + return res.status(200).json(adList); + } + + default: { + return res.status(405).send(''); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/api/products/[productId]/index.ts b/src/frontend/pages/api/products/[productId]/index.ts new file mode 100644 index 0000000..eb62465 --- /dev/null +++ b/src/frontend/pages/api/products/[productId]/index.ts @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiRequest, NextApiResponse } from 'next'; +import InstrumentationMiddleware from '../../../../utils/telemetry/InstrumentationMiddleware'; +import { Empty, Product } from '../../../../protos/demo'; +import ProductCatalogService from '../../../../services/ProductCatalog.service'; + +type TResponse = Product | Empty; + +const handler = async ({ method, query }: NextApiRequest, res: NextApiResponse<TResponse>) => { + switch (method) { + case 'GET': { + const { productId = '', currencyCode = '' } = query; + const product = await ProductCatalogService.getProduct(productId as string, currencyCode as string); + + return res.status(200).json(product); + } + + default: { + return res.status(405).send(''); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/api/products/index.ts b/src/frontend/pages/api/products/index.ts new file mode 100644 index 0000000..74b8937 --- /dev/null +++ b/src/frontend/pages/api/products/index.ts @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiRequest, NextApiResponse } from 'next'; +import InstrumentationMiddleware from '../../../utils/telemetry/InstrumentationMiddleware'; +import { Empty, Product } from '../../../protos/demo'; +import ProductCatalogService from '../../../services/ProductCatalog.service'; + +type TResponse = Product[] | Empty; + +const handler = async ({ method, query }: NextApiRequest, res: NextApiResponse<TResponse>) => { + switch (method) { + case 'GET': { + const { currencyCode = '' } = query; + const productList = await ProductCatalogService.listProducts(currencyCode as string); + + return res.status(200).json(productList); + } + + default: { + return res.status(405).send(''); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/api/recommendations.ts b/src/frontend/pages/api/recommendations.ts new file mode 100644 index 0000000..dd975a9 --- /dev/null +++ b/src/frontend/pages/api/recommendations.ts @@ -0,0 +1,33 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiRequest, NextApiResponse } from 'next'; +import InstrumentationMiddleware from '../../utils/telemetry/InstrumentationMiddleware'; +import RecommendationsGateway from '../../gateways/rpc/Recommendations.gateway'; +import { Empty, Product } from '../../protos/demo'; +import ProductCatalogService from '../../services/ProductCatalog.service'; + +type TResponse = Product[] | Empty; + +const handler = async ({ method, query }: NextApiRequest, res: NextApiResponse<TResponse>) => { + switch (method) { + case 'GET': { + const { productIds = [], sessionId = '', currencyCode = '' } = query; + const { productIds: productList } = await RecommendationsGateway.listRecommendations( + sessionId as string, + productIds as string[] + ); + const recommendedProductList = await Promise.all( + productList.slice(0, 4).map(id => ProductCatalogService.getProduct(id, currencyCode as string)) + ); + + return res.status(200).json(recommendedProductList); + } + + default: { + return res.status(405).send(''); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/api/shipping.ts b/src/frontend/pages/api/shipping.ts new file mode 100644 index 0000000..3a29001 --- /dev/null +++ b/src/frontend/pages/api/shipping.ts @@ -0,0 +1,29 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import type { NextApiRequest, NextApiResponse } from 'next'; +import InstrumentationMiddleware from '../../utils/telemetry/InstrumentationMiddleware'; +import ShippingGateway from '../../gateways/http/Shipping.gateway'; +import { Address, CartItem, Empty, Money } from '../../protos/demo'; +import CurrencyGateway from '../../gateways/rpc/Currency.gateway'; + +type TResponse = Money | Empty; + +const handler = async ({ method, query }: NextApiRequest, res: NextApiResponse<TResponse>) => { + switch (method) { + case 'GET': { + const { itemList = '', currencyCode = 'USD', address = '' } = query; + const { costUsd } = await ShippingGateway.getShippingCost(JSON.parse(itemList as string) as CartItem[], + JSON.parse(address as string) as Address); + const cost = await CurrencyGateway.convert(costUsd!, currencyCode as string); + + return res.status(200).json(cost!); + } + + default: { + return res.status(405); + } + } +}; + +export default InstrumentationMiddleware(handler); diff --git a/src/frontend/pages/cart/checkout/[orderId]/index.tsx b/src/frontend/pages/cart/checkout/[orderId]/index.tsx new file mode 100644 index 0000000..740b895 --- /dev/null +++ b/src/frontend/pages/cart/checkout/[orderId]/index.tsx @@ -0,0 +1,61 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { NextPage } from 'next'; +import Head from 'next/head'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import Ad from '../../../../components/Ad'; +import Button from '../../../../components/Button'; +import CheckoutItem from '../../../../components/CheckoutItem'; +import Footer from '../../../../components/Footer'; +import Layout from '../../../../components/Layout'; +import Recommendations from '../../../../components/Recommendations'; +import AdProvider from '../../../../providers/Ad.provider'; +import * as S from '../../../../styles/Checkout.styled'; +import { IProductCheckout } from '../../../../types/Cart'; + +const Checkout: NextPage = () => { + const { query } = useRouter(); + const { items = [], shippingAddress } = JSON.parse((query.order || '{}') as string) as IProductCheckout; + + return ( + <AdProvider + productIds={items.map(({ item }) => item?.productId || '')} + contextKeys={[...new Set(items.flatMap(({ item }) => item.product.categories))]} + > + <Head> + <title>Otel Demo - Checkout</title> + </Head> + <Layout> + <S.Checkout> + <S.Container> + <S.Title>Your order is complete!</S.Title> + <S.Subtitle>We've sent you a confirmation email.</S.Subtitle> + + <S.ItemList> + {items.map(checkoutItem => ( + <CheckoutItem + key={checkoutItem.item.productId} + checkoutItem={checkoutItem} + address={shippingAddress} + /> + ))} + </S.ItemList> + + <S.ButtonContainer> + <Link href="/"> + <Button type="submit">Continue Shopping</Button> + </Link> + </S.ButtonContainer> + </S.Container> + <Recommendations /> + </S.Checkout> + <Ad /> + <Footer /> + </Layout> + </AdProvider> + ); +}; + +export default Checkout; diff --git a/src/frontend/pages/cart/index.tsx b/src/frontend/pages/cart/index.tsx new file mode 100644 index 0000000..efb01c9 --- /dev/null +++ b/src/frontend/pages/cart/index.tsx @@ -0,0 +1,39 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { NextPage } from 'next'; +import Head from 'next/head'; +import Footer from '../../components/Footer'; +import Layout from '../../components/Layout'; +import Recommendations from '../../components/Recommendations'; +import * as S from '../../styles/Cart.styled'; +import CartDetail from '../../components/Cart/CartDetail'; +import EmptyCart from '../../components/Cart/EmptyCart'; +import { useCart } from '../../providers/Cart.provider'; +import AdProvider from '../../providers/Ad.provider'; + +const Cart: NextPage = () => { + const { + cart: { items }, + } = useCart(); + + return ( + <AdProvider + productIds={items.map(({ productId }) => productId)} + contextKeys={[...new Set(items.flatMap(({ product }) => product.categories))]} + > + <Head> + <title>Otel Demo - Cart</title> + </Head> + <Layout> + <S.Cart> + {(!!items.length && <CartDetail />) || <EmptyCart />} + <Recommendations /> + </S.Cart> + <Footer /> + </Layout> + </AdProvider> + ); +}; + +export default Cart; diff --git a/src/frontend/pages/index.tsx b/src/frontend/pages/index.tsx new file mode 100755 index 0000000..6ec2007 --- /dev/null +++ b/src/frontend/pages/index.tsx @@ -0,0 +1,48 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { NextPage } from 'next'; +import Head from 'next/head'; +import Footer from '../components/Footer'; +import Layout from '../components/Layout'; +import ProductList from '../components/ProductList'; +import * as S from '../styles/Home.styled'; +import { useQuery } from '@tanstack/react-query'; +import ApiGateway from '../gateways/Api.gateway'; +import Banner from '../components/Banner'; +import { CypressFields } from '../utils/enums/CypressFields'; +import { useCurrency } from '../providers/Currency.provider'; + +const Home: NextPage = () => { + const { selectedCurrency } = useCurrency(); + const { data: productList = [] } = useQuery({ + queryKey: ['products', selectedCurrency], + queryFn: () => ApiGateway.listProducts(selectedCurrency), + }); + + return ( + <Layout> + <Head> + <title>Otel Demo - Home</title> + </Head> + <S.Home data-cy={CypressFields.HomePage}> + <Banner /> + <S.Container> + <S.Row> + <S.Content> + <S.HotProducts> + <S.HotProductsTitle data-cy={CypressFields.HotProducts} id="hot-products"> + Hot Products + </S.HotProductsTitle> + <ProductList productList={productList} /> + </S.HotProducts> + </S.Content> + </S.Row> + </S.Container> + <Footer /> + </S.Home> + </Layout> + ); +}; + +export default Home; diff --git a/src/frontend/pages/product/[productId]/index.tsx b/src/frontend/pages/product/[productId]/index.tsx new file mode 100644 index 0000000..15766c8 --- /dev/null +++ b/src/frontend/pages/product/[productId]/index.tsx @@ -0,0 +1,107 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +import { NextPage } from 'next'; +import Head from 'next/head'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { useCallback, useState, useEffect } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import Ad from '../../../components/Ad'; +import Footer from '../../../components/Footer'; +import Layout from '../../../components/Layout'; +import ProductPrice from '../../../components/ProductPrice'; +import Recommendations from '../../../components/Recommendations'; +import Select from '../../../components/Select'; +import { CypressFields } from '../../../utils/enums/CypressFields'; +import ApiGateway from '../../../gateways/Api.gateway'; +import { Product } from '../../../protos/demo'; +import AdProvider from '../../../providers/Ad.provider'; +import { useCart } from '../../../providers/Cart.provider'; +import * as S from '../../../styles/ProductDetail.styled'; +import { useCurrency } from '../../../providers/Currency.provider'; + +const quantityOptions = new Array(10).fill(0).map((_, i) => i + 1); + +const ProductDetail: NextPage = () => { + const { push, query } = useRouter(); + const [quantity, setQuantity] = useState(1); + const { + addItem, + cart: { items }, + } = useCart(); + const { selectedCurrency } = useCurrency(); + const productId = query.productId as string; + + useEffect(() => { + setQuantity(1); + }, [productId]); + + const { + data: { + name, + picture, + description, + priceUsd = { units: 0, currencyCode: 'USD', nanos: 0 }, + categories, + } = {} as Product, + } = useQuery({ + queryKey: ['product', productId, 'selectedCurrency', selectedCurrency], + queryFn: () => ApiGateway.getProduct(productId, selectedCurrency), + enabled: !!productId, + } + ) as { data: Product }; + + const onAddItem = useCallback(async () => { + await addItem({ + productId, + quantity, + }); + push('/cart'); + }, [addItem, productId, quantity, push]); + + return ( + <AdProvider + productIds={[productId, ...items.map(({ productId }) => productId)]} + contextKeys={[...new Set(categories)]} + > + <Head> + <title>Otel Demo - Product</title> + </Head> + <Layout> + <S.ProductDetail data-cy={CypressFields.ProductDetail}> + <S.Container> + <S.Image $src={"/images/products/" + picture} data-cy={CypressFields.ProductPicture} /> + <S.Details> + <S.Name data-cy={CypressFields.ProductName}>{name}</S.Name> + <S.Description data-cy={CypressFields.ProductDescription}>{description}</S.Description> + <S.ProductPrice> + <ProductPrice price={priceUsd} /> + </S.ProductPrice> + <S.Text>Quantity</S.Text> + <Select + data-cy={CypressFields.ProductQuantity} + onChange={event => setQuantity(+event.target.value)} + value={quantity} + > + {quantityOptions.map(option => ( + <option key={option} value={option}> + {option} + </option> + ))} + </Select> + <S.AddToCart data-cy={CypressFields.ProductAddToCart} onClick={onAddItem}> + <Image src="/icons/Cart.svg" height="15" width="15" alt="cart" /> Add To Cart + </S.AddToCart> + </S.Details> + </S.Container> + <Recommendations /> + </S.ProductDetail> + <Ad /> + <Footer /> + </Layout> + </AdProvider> + ); +}; + +export default ProductDetail; |
