summaryrefslogtreecommitdiff
path: root/src/frontend/pages
diff options
context:
space:
mode:
authorSaumit <justsaumit@protonmail.com>2025-09-27 02:14:26 +0530
committerSaumit <justsaumit@protonmail.com>2025-09-27 02:14:26 +0530
commit82e03978b89938219958032efb1448cc76baa181 (patch)
tree626f3e54d52ecd49be0ed3bee30abacc0453d081 /src/frontend/pages
Initial snapshot - OpenTelemetry demo 2.1.3 -f
Diffstat (limited to 'src/frontend/pages')
-rwxr-xr-xsrc/frontend/pages/_app.tsx83
-rw-r--r--src/frontend/pages/_document.tsx65
-rwxr-xr-xsrc/frontend/pages/api/cart.ts56
-rw-r--r--src/frontend/pages/api/checkout.ts44
-rw-r--r--src/frontend/pages/api/currency.ts25
-rw-r--r--src/frontend/pages/api/data.ts26
-rw-r--r--src/frontend/pages/api/products/[productId]/index.ts26
-rw-r--r--src/frontend/pages/api/products/index.ts26
-rw-r--r--src/frontend/pages/api/recommendations.ts33
-rw-r--r--src/frontend/pages/api/shipping.ts29
-rw-r--r--src/frontend/pages/cart/checkout/[orderId]/index.tsx61
-rw-r--r--src/frontend/pages/cart/index.tsx39
-rwxr-xr-xsrc/frontend/pages/index.tsx48
-rw-r--r--src/frontend/pages/product/[productId]/index.tsx107
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&apos;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;