summaryrefslogtreecommitdiff
path: root/src/frontend/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/frontend/components')
-rw-r--r--src/frontend/components/Ad/Ad.styled.ts24
-rw-r--r--src/frontend/components/Ad/Ad.tsx21
-rw-r--r--src/frontend/components/Ad/index.ts4
-rw-r--r--src/frontend/components/Banner/Banner.styled.ts59
-rw-r--r--src/frontend/components/Banner/Banner.tsx21
-rw-r--r--src/frontend/components/Banner/index.ts4
-rw-r--r--src/frontend/components/Button/Button.tsx36
-rw-r--r--src/frontend/components/Button/index.ts4
-rw-r--r--src/frontend/components/Cart/CartDetail.tsx81
-rw-r--r--src/frontend/components/Cart/EmptyCart.tsx23
-rw-r--r--src/frontend/components/CartDropdown/CartDropdown.styled.ts100
-rw-r--r--src/frontend/components/CartDropdown/CartDropdown.tsx65
-rw-r--r--src/frontend/components/CartDropdown/index.ts4
-rw-r--r--src/frontend/components/CartIcon/CartIcon.styled.ts39
-rw-r--r--src/frontend/components/CartIcon/CartIcon.tsx27
-rw-r--r--src/frontend/components/CartIcon/index.ts4
-rw-r--r--src/frontend/components/CartItems/CartItem.tsx40
-rw-r--r--src/frontend/components/CartItems/CartItems.styled.ts79
-rw-r--r--src/frontend/components/CartItems/CartItems.tsx82
-rw-r--r--src/frontend/components/CartItems/index.ts4
-rw-r--r--src/frontend/components/CheckoutForm/CheckoutForm.styled.ts59
-rw-r--r--src/frontend/components/CheckoutForm/CheckoutForm.tsx202
-rw-r--r--src/frontend/components/CheckoutForm/index.ts4
-rw-r--r--src/frontend/components/CheckoutItem/CheckoutItem.styled.ts90
-rw-r--r--src/frontend/components/CheckoutItem/CheckoutItem.tsx61
-rw-r--r--src/frontend/components/CheckoutItem/index.ts4
-rw-r--r--src/frontend/components/CurrencySwitcher/CurrencySwitcher.styled.ts65
-rw-r--r--src/frontend/components/CurrencySwitcher/CurrencySwitcher.tsx37
-rw-r--r--src/frontend/components/CurrencySwitcher/index.ts4
-rw-r--r--src/frontend/components/Footer/Footer.styled.ts16
-rw-r--r--src/frontend/components/Footer/Footer.tsx37
-rw-r--r--src/frontend/components/Footer/index.ts4
-rw-r--r--src/frontend/components/Header/Header.styled.ts55
-rw-r--r--src/frontend/components/Header/Header.tsx26
-rw-r--r--src/frontend/components/Header/index.ts4
-rw-r--r--src/frontend/components/Input/Input.styled.ts54
-rw-r--r--src/frontend/components/Input/Input.tsx31
-rw-r--r--src/frontend/components/Input/index.ts4
-rw-r--r--src/frontend/components/Layout/Layout.tsx19
-rw-r--r--src/frontend/components/Layout/index.ts4
-rw-r--r--src/frontend/components/PlatformFlag/PlatformFlag.styled.ts25
-rw-r--r--src/frontend/components/PlatformFlag/PlatformFlag.tsx16
-rw-r--r--src/frontend/components/PlatformFlag/index.ts4
-rw-r--r--src/frontend/components/ProductCard/ProductCard.styled.ts36
-rw-r--r--src/frontend/components/ProductCard/ProductCard.tsx65
-rw-r--r--src/frontend/components/ProductCard/index.ts4
-rw-r--r--src/frontend/components/ProductList/ProductList.styled.ts14
-rw-r--r--src/frontend/components/ProductList/ProductList.tsx23
-rw-r--r--src/frontend/components/ProductList/index.ts4
-rw-r--r--src/frontend/components/ProductPrice/ProductPrice.tsx31
-rw-r--r--src/frontend/components/ProductPrice/index.ts4
-rw-r--r--src/frontend/components/Recommendations/Recommendations.styled.ts39
-rw-r--r--src/frontend/components/Recommendations/Recommendations.tsx26
-rw-r--r--src/frontend/components/Recommendations/index.ts4
-rw-r--r--src/frontend/components/Select/Select.styled.ts31
-rw-r--r--src/frontend/components/Select/Select.tsx20
-rw-r--r--src/frontend/components/Select/index.ts4
57 files changed, 1851 insertions, 0 deletions
diff --git a/src/frontend/components/Ad/Ad.styled.ts b/src/frontend/components/Ad/Ad.styled.ts
new file mode 100644
index 0000000..2940e58
--- /dev/null
+++ b/src/frontend/components/Ad/Ad.styled.ts
@@ -0,0 +1,24 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+import RouterLink from 'next/link';
+
+export const Ad = styled.section`
+ position: relative;
+ background-color: ${({ theme }) => theme.colors.otelYellow};
+ font-size: ${({ theme }) => theme.sizes.dMedium};
+ text-align: center;
+ padding: 48px;
+
+ * {
+ color: ${({ theme }) => theme.colors.white};
+ margin: 0;
+ cursor: pointer;
+ }
+`;
+
+export const Link = styled(RouterLink)`
+ color: black;
+ text-decoration: none;
+`;
diff --git a/src/frontend/components/Ad/Ad.tsx b/src/frontend/components/Ad/Ad.tsx
new file mode 100644
index 0000000..6a5ae14
--- /dev/null
+++ b/src/frontend/components/Ad/Ad.tsx
@@ -0,0 +1,21 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { CypressFields } from '../../utils/enums/CypressFields';
+import { useAd } from '../../providers/Ad.provider';
+import * as S from './Ad.styled';
+
+const Ad = () => {
+ const { adList } = useAd();
+ const { text, redirectUrl } = adList[Math.floor(Math.random() * adList.length)] || { text: '', redirectUrl: '' };
+
+ return (
+ <S.Ad data-cy={CypressFields.Ad}>
+ <S.Link href={redirectUrl}>
+ <p>{text}</p>
+ </S.Link>
+ </S.Ad>
+ );
+};
+
+export default Ad;
diff --git a/src/frontend/components/Ad/index.ts b/src/frontend/components/Ad/index.ts
new file mode 100644
index 0000000..b64732f
--- /dev/null
+++ b/src/frontend/components/Ad/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './Ad';
diff --git a/src/frontend/components/Banner/Banner.styled.ts b/src/frontend/components/Banner/Banner.styled.ts
new file mode 100644
index 0000000..6aa7d62
--- /dev/null
+++ b/src/frontend/components/Banner/Banner.styled.ts
@@ -0,0 +1,59 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+import Button from '../Button';
+
+export const Banner = styled.div`
+ display: flex;
+ flex-direction: column;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ flex-direction: row-reverse;
+ padding-bottom: 38px;
+ background: ${({ theme }) => theme.colors.backgroundGray};
+ }
+`;
+
+export const BannerImg = styled.img.attrs({
+ src: '/images/Banner.png',
+})`
+ width: 100%;
+ height: auto;
+`;
+
+export const ImageContainer = styled.div`
+ ${({ theme }) => theme.breakpoints.desktop} {
+ min-width: 50%;
+ }
+`;
+
+export const TextContainer = styled.div`
+ padding: 20px;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: start;
+ width: 50%;
+ padding: 100px 160px 100px 100px;
+ }
+`;
+
+export const Title = styled.h1`
+ font-size: ${({ theme }) => theme.sizes.mxLarge};
+ font-weight: ${({ theme }) => theme.fonts.bold};
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ font-size: ${({ theme }) => theme.sizes.dxLarge};
+ }
+`;
+
+export const GoShoppingButton = styled(Button)`
+ width: 100%;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ width: auto;
+ }
+`;
diff --git a/src/frontend/components/Banner/Banner.tsx b/src/frontend/components/Banner/Banner.tsx
new file mode 100644
index 0000000..f2d75e2
--- /dev/null
+++ b/src/frontend/components/Banner/Banner.tsx
@@ -0,0 +1,21 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link';
+import * as S from './Banner.styled';
+
+const Banner = () => {
+ return (
+ <S.Banner>
+ <S.ImageContainer>
+ <S.BannerImg />
+ </S.ImageContainer>
+ <S.TextContainer>
+ <S.Title>The best telescopes to see the world closer</S.Title>
+ <Link href="#hot-products"><S.GoShoppingButton>Go Shopping</S.GoShoppingButton></Link>
+ </S.TextContainer>
+ </S.Banner>
+ );
+};
+
+export default Banner;
diff --git a/src/frontend/components/Banner/index.ts b/src/frontend/components/Banner/index.ts
new file mode 100644
index 0000000..1d87266
--- /dev/null
+++ b/src/frontend/components/Banner/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './Banner';
diff --git a/src/frontend/components/Button/Button.tsx b/src/frontend/components/Button/Button.tsx
new file mode 100644
index 0000000..0807750
--- /dev/null
+++ b/src/frontend/components/Button/Button.tsx
@@ -0,0 +1,36 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled, { css } from 'styled-components';
+
+const Button = styled.button<{ $type?: 'primary' | 'secondary' | 'link' }>`
+ background-color: #5262a8;
+ color: white;
+ display: inline-block;
+ border: solid 1px #5262a8;
+ padding: 8px 16px;
+ outline: none;
+ font-weight: 700;
+ font-size: 20px;
+ line-height: 27px;
+ border-radius: 10px;
+ height: 62px;
+ cursor: pointer;
+
+ ${({ $type = 'primary' }) =>
+ $type === 'secondary' &&
+ css`
+ background: none;
+ color: #5262a8;
+ `};
+
+ ${({ $type = 'primary' }) =>
+ $type === 'link' &&
+ css`
+ background: none;
+ color: #5262a8;
+ border: none;
+ `};
+`;
+
+export default Button;
diff --git a/src/frontend/components/Button/index.ts b/src/frontend/components/Button/index.ts
new file mode 100644
index 0000000..0af447b
--- /dev/null
+++ b/src/frontend/components/Button/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './Button';
diff --git a/src/frontend/components/Cart/CartDetail.tsx b/src/frontend/components/Cart/CartDetail.tsx
new file mode 100644
index 0000000..4d777e4
--- /dev/null
+++ b/src/frontend/components/Cart/CartDetail.tsx
@@ -0,0 +1,81 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { useRouter } from 'next/router';
+import { useCallback } from 'react';
+import CartItems from '../CartItems';
+import CheckoutForm from '../CheckoutForm';
+import { IFormData } from '../CheckoutForm/CheckoutForm';
+import SessionGateway from '../../gateways/Session.gateway';
+import { useCart } from '../../providers/Cart.provider';
+import { useCurrency } from '../../providers/Currency.provider';
+import * as S from '../../styles/Cart.styled';
+
+const { userId } = SessionGateway.getSession();
+
+const CartDetail = () => {
+ const {
+ cart: { items },
+ emptyCart,
+ placeOrder,
+ } = useCart();
+ const { selectedCurrency } = useCurrency();
+ const { push } = useRouter();
+
+ const onPlaceOrder = useCallback(
+ async ({
+ email,
+ state,
+ streetAddress,
+ country,
+ city,
+ zipCode,
+ creditCardCvv,
+ creditCardExpirationMonth,
+ creditCardExpirationYear,
+ creditCardNumber,
+ }: IFormData) => {
+ const order = await placeOrder({
+ userId,
+ email,
+ address: {
+ streetAddress,
+ state,
+ country,
+ city,
+ zipCode,
+ },
+ userCurrency: selectedCurrency,
+ creditCard: {
+ creditCardCvv,
+ creditCardExpirationMonth,
+ creditCardExpirationYear,
+ creditCardNumber,
+ },
+ });
+
+ push({
+ pathname: `/cart/checkout/${order.orderId}`,
+ query: { order: JSON.stringify(order) },
+ });
+ },
+ [placeOrder, push, selectedCurrency]
+ );
+
+ return (
+ <S.Container>
+ <div>
+ <S.Header>
+ <S.CarTitle>Shopping Cart</S.CarTitle>
+ <S.EmptyCartButton onClick={emptyCart} $type="link">
+ Empty Cart
+ </S.EmptyCartButton>
+ </S.Header>
+ <CartItems productList={items} />
+ </div>
+ <CheckoutForm onSubmit={onPlaceOrder} />
+ </S.Container>
+ );
+};
+
+export default CartDetail;
diff --git a/src/frontend/components/Cart/EmptyCart.tsx b/src/frontend/components/Cart/EmptyCart.tsx
new file mode 100644
index 0000000..70f403f
--- /dev/null
+++ b/src/frontend/components/Cart/EmptyCart.tsx
@@ -0,0 +1,23 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link';
+import Button from '../Button';
+import * as S from '../../styles/Cart.styled';
+
+const EmptyCart = () => {
+ return (
+ <S.EmptyCartContainer>
+ <S.Title>Your shopping cart is empty!</S.Title>
+ <S.Subtitle>Items you add to your shopping cart will appear here.</S.Subtitle>
+
+ <S.ButtonContainer>
+ <Link href="/">
+ <Button type="submit">Continue Shopping</Button>
+ </Link>
+ </S.ButtonContainer>
+ </S.EmptyCartContainer>
+ );
+};
+
+export default EmptyCart;
diff --git a/src/frontend/components/CartDropdown/CartDropdown.styled.ts b/src/frontend/components/CartDropdown/CartDropdown.styled.ts
new file mode 100644
index 0000000..8820210
--- /dev/null
+++ b/src/frontend/components/CartDropdown/CartDropdown.styled.ts
@@ -0,0 +1,100 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Image from 'next/image';
+import styled from 'styled-components';
+import Button from '../Button';
+
+export const CartDropdown = styled.div`
+ position: fixed;
+ top: 0;
+ right: 0;
+ width: 100%;
+ height: 100%;
+ max-height: 100%;
+ padding: 25px;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 24px;
+ background: ${({ theme }) => theme.colors.white};
+ z-index: 1000;
+ border-radius: 5px;
+ box-shadow: 0 2px 2px 0 rgb(0 0 0 / 14%), 0 3px 1px -2px rgb(0 0 0 / 12%), 0 1px 5px 0 rgb(0 0 0 / 20%);
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ position: absolute;
+ width: 400px;
+ top: 95px;
+ right: 17px;
+ max-height: 650px;
+ }
+`;
+
+export const Title = styled.h5`
+ margin: 0px;
+ font-size: ${({ theme }) => theme.sizes.mxLarge};
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ font-size: ${({ theme }) => theme.sizes.dLarge};
+ }
+`;
+
+export const ItemList = styled.div`
+ ${({ theme }) => theme.breakpoints.desktop} {
+ max-height: 450px;
+ overflow-y: scroll;
+ }
+`;
+
+export const Item = styled.div`
+ display: grid;
+ grid-template-columns: 29% 59%;
+ gap: 2%;
+ padding: 25px 0;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.textLightGray};
+`;
+
+export const ItemImage = styled(Image).attrs({
+ width: '80',
+ height: '80',
+})`
+ border-radius: 5px;
+`;
+
+export const ItemName = styled.p`
+ margin: 0px;
+ font-size: ${({ theme }) => theme.sizes.mLarge};
+ font-weight: ${({ theme }) => theme.fonts.regular};
+`;
+
+export const ItemDetails = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+`;
+
+export const ItemQuantity = styled(ItemName)`
+ font-size: ${({ theme }) => theme.sizes.mMedium};
+`;
+
+export const CartButton = styled(Button)``;
+
+export const Header = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ span {
+ display: none;
+ }
+ }
+`;
+
+export const EmptyCart = styled.h3`
+ margin: 0;
+ margin-top: 25px;
+ font-size: ${({ theme }) => theme.sizes.mLarge};
+ color: ${({ theme }) => theme.colors.textLightGray};
+`;
diff --git a/src/frontend/components/CartDropdown/CartDropdown.tsx b/src/frontend/components/CartDropdown/CartDropdown.tsx
new file mode 100644
index 0000000..cd0703e
--- /dev/null
+++ b/src/frontend/components/CartDropdown/CartDropdown.tsx
@@ -0,0 +1,65 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link';
+import { useEffect, useRef } from 'react';
+import { CypressFields } from '../../utils/enums/CypressFields';
+import { IProductCartItem } from '../../types/Cart';
+import ProductPrice from '../ProductPrice';
+import * as S from './CartDropdown.styled';
+
+interface IProps {
+ isOpen: boolean;
+ onClose(): void;
+ productList: IProductCartItem[];
+}
+
+const CartDropdown = ({ productList, isOpen, onClose }: IProps) => {
+ const ref = useRef<HTMLDivElement>(null);
+
+ useEffect(() => {
+ const handleClickOutside = (event: Event) => {
+ if (ref.current && !ref.current.contains(event.target as Node)) {
+ onClose();
+ }
+ };
+ // Bind the event listener
+ document.addEventListener('mousedown', handleClickOutside);
+
+ return () => {
+ // Unbind the event listener on clean up
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, [ref, onClose]);
+
+ return isOpen ? (
+ <S.CartDropdown ref={ref} data-cy={CypressFields.CartDropdown}>
+ <div>
+ <S.Header>
+ <S.Title>Shopping Cart</S.Title>
+ <span onClick={onClose}>Close</span>
+ </S.Header>
+ <S.ItemList>
+ {!productList.length && <S.EmptyCart>Your shopping cart is empty</S.EmptyCart>}
+ {productList.map(
+ ({ quantity, product: { name, picture, id, priceUsd = { nanos: 0, currencyCode: 'USD', units: 0 } } }) => (
+ <S.Item key={id} data-cy={CypressFields.CartDropdownItem}>
+ <S.ItemImage src={"/images/products/" + picture} alt={name} />
+ <S.ItemDetails>
+ <S.ItemName>{name}</S.ItemName>
+ <ProductPrice price={priceUsd} />
+ <S.ItemQuantity>Quantity: {quantity}</S.ItemQuantity>
+ </S.ItemDetails>
+ </S.Item>
+ )
+ )}
+ </S.ItemList>
+ </div>
+ <Link href="/cart">
+ <S.CartButton data-cy={CypressFields.CartGoToShopping}>Go to Shopping Cart</S.CartButton>
+ </Link>
+ </S.CartDropdown>
+ ) : null;
+};
+
+export default CartDropdown;
diff --git a/src/frontend/components/CartDropdown/index.ts b/src/frontend/components/CartDropdown/index.ts
new file mode 100644
index 0000000..559fff3
--- /dev/null
+++ b/src/frontend/components/CartDropdown/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './CartDropdown';
diff --git a/src/frontend/components/CartIcon/CartIcon.styled.ts b/src/frontend/components/CartIcon/CartIcon.styled.ts
new file mode 100644
index 0000000..d76e4fd
--- /dev/null
+++ b/src/frontend/components/CartIcon/CartIcon.styled.ts
@@ -0,0 +1,39 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Image from 'next/image';
+import styled from 'styled-components';
+
+export const CartIcon = styled.a`
+ position: relative;
+ display: block;
+ margin-left: 25px;
+ display: flex;
+ flex-flow: column;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+`;
+
+export const Icon = styled(Image).attrs({
+ width: '24',
+ height: '24',
+})`
+ margin-bottom: 3px;
+`;
+
+export const ItemsCount = styled.span`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ position: absolute;
+ top: 9px;
+ left: 15px;
+ width: 15px;
+ height: 15px;
+ font-size: ${({ theme }) => theme.sizes.nano};
+ border-radius: 50%;
+ border: 1px solid ${({ theme }) => theme.colors.white};
+ color: ${({ theme }) => theme.colors.white};
+ background: ${({ theme }) => theme.colors.otelRed};
+`;
diff --git a/src/frontend/components/CartIcon/CartIcon.tsx b/src/frontend/components/CartIcon/CartIcon.tsx
new file mode 100644
index 0000000..8ec128a
--- /dev/null
+++ b/src/frontend/components/CartIcon/CartIcon.tsx
@@ -0,0 +1,27 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { useState } from 'react';
+import { CypressFields } from '../../utils/enums/CypressFields';
+import { useCart } from '../../providers/Cart.provider';
+import CartDropdown from '../CartDropdown';
+import * as S from './CartIcon.styled';
+
+const CartIcon = () => {
+ const [isOpen, setIsOpen] = useState(false);
+ const {
+ cart: { items },
+ } = useCart();
+
+ return (
+ <>
+ <S.CartIcon data-cy={CypressFields.CartIcon} onClick={() => setIsOpen(true)}>
+ <S.Icon src="/icons/CartIcon.svg" alt="Cart icon" title="Cart" />
+ {!!items.length && <S.ItemsCount data-cy={CypressFields.CartItemCount}>{items.length}</S.ItemsCount>}
+ </S.CartIcon>
+ <CartDropdown productList={items} isOpen={isOpen} onClose={() => setIsOpen(false)} />
+ </>
+ );
+};
+
+export default CartIcon;
diff --git a/src/frontend/components/CartIcon/index.ts b/src/frontend/components/CartIcon/index.ts
new file mode 100644
index 0000000..ffb59cc
--- /dev/null
+++ b/src/frontend/components/CartIcon/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './CartIcon';
diff --git a/src/frontend/components/CartItems/CartItem.tsx b/src/frontend/components/CartItems/CartItem.tsx
new file mode 100644
index 0000000..e78bb7a
--- /dev/null
+++ b/src/frontend/components/CartItems/CartItem.tsx
@@ -0,0 +1,40 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link';
+import { Product } from '../../protos/demo';
+import ProductPrice from '../ProductPrice';
+import * as S from './CartItems.styled';
+
+interface IProps {
+ product: Product;
+ quantity: number;
+}
+
+const CartItem = ({
+ product: { id, name, picture, priceUsd = { units: 0, nanos: 0, currencyCode: 'USD' } },
+ quantity,
+}: IProps) => {
+ return (
+ <S.CartItem>
+ <Link href={`/product/${id}`}>
+ <S.NameContainer>
+ <S.CartItemImage alt={name} src={"/images/products/" + picture} />
+ <p>{name}</p>
+ </S.NameContainer>
+ </Link>
+ <S.CartItemDetails>
+ <p>{quantity}</p>
+ </S.CartItemDetails>
+ <S.CartItemDetails>
+ <S.PriceContainer>
+ <p>
+ <ProductPrice price={priceUsd} />
+ </p>
+ </S.PriceContainer>
+ </S.CartItemDetails>
+ </S.CartItem>
+ );
+};
+
+export default CartItem;
diff --git a/src/frontend/components/CartItems/CartItems.styled.ts b/src/frontend/components/CartItems/CartItems.styled.ts
new file mode 100644
index 0000000..a1a5fbc
--- /dev/null
+++ b/src/frontend/components/CartItems/CartItems.styled.ts
@@ -0,0 +1,79 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const CartItems = styled.section`
+ display: flex;
+ flex-direction: column;
+`;
+
+export const CardItemsHeader = styled.div`
+ display: grid;
+ grid-template-columns: 150px 100px auto;
+ gap: 24px;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ grid-template-columns: 1fr auto auto;
+ }
+`;
+
+export const CartItemImage = styled.img`
+ width: 100%;
+ height: auto;
+ border-radius: 5px;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ width: 120px;
+ height: 120px;
+ }
+`;
+
+export const CartItem = styled.div`
+ display: grid;
+ grid-template-columns: 150px 100px auto;
+ gap: 24px;
+ padding: 24px 0;
+ align-items: center;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.textLightGray};
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ grid-template-columns: 1fr auto auto;
+ }
+`;
+
+export const CartItemDetails = styled.div`
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+`;
+
+export const NameContainer = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 5px;
+ flex-direction: column;
+ cursor: pointer;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ flex-direction: row;
+ gap: 24px;
+ }
+`;
+
+export const PriceContainer = styled.div`
+ display: flex;
+ width: 100%;
+ justify-content: space-between;
+`;
+
+export const DataRow = styled.div`
+ display: flex;
+ justify-content: flex-end;
+ padding: 24px 0;
+ gap: 24px;
+`;
+
+export const TotalText = styled.h3`
+ margin: 0;
+`;
diff --git a/src/frontend/components/CartItems/CartItems.tsx b/src/frontend/components/CartItems/CartItems.tsx
new file mode 100644
index 0000000..05e0279
--- /dev/null
+++ b/src/frontend/components/CartItems/CartItems.tsx
@@ -0,0 +1,82 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { useMemo } from 'react';
+import { useQuery, UseQueryOptions } from '@tanstack/react-query';
+import ApiGateway from '../../gateways/Api.gateway';
+import { Address, Money } from '../../protos/demo';
+import { useCurrency } from '../../providers/Currency.provider';
+import { IProductCartItem } from '../../types/Cart';
+import ProductPrice from '../ProductPrice';
+import CartItem from './CartItem';
+import * as S from './CartItems.styled';
+
+interface IProps {
+ productList: IProductCartItem[];
+ shouldShowPrice?: boolean;
+}
+
+const CartItems = ({ productList, shouldShowPrice = true }: IProps) => {
+ const { selectedCurrency } = useCurrency();
+ const address: Address = {
+ streetAddress: '1600 Amphitheatre Parkway',
+ city: 'Mountain View',
+ state: 'CA',
+ country: 'United States',
+ zipCode: '94043',
+ };
+
+ const queryKey = ['shipping', productList, selectedCurrency, address];
+ const queryFn = () => ApiGateway.getShippingCost(productList, selectedCurrency, address);
+ const queryOptions: UseQueryOptions<Money, Error> = {
+ queryKey,
+ queryFn,
+ };
+ const { data: shippingConst = { units: 0, currencyCode: 'USD', nanos: 0 } } = useQuery(queryOptions);
+
+ const total = useMemo<Money>(() => {
+ const nanoSum =
+ productList.reduce((acc, { product: { priceUsd: { nanos = 0 } = {} } }) => acc + Number(nanos), 0) +
+ shippingConst?.nanos || 0;
+ const nanoExceed = Math.floor(nanoSum / 1000000000);
+
+ const unitSum =
+ productList.reduce((acc, { product: { priceUsd: { units = 0 } = {} } }) => acc + Number(units), 0) +
+ (shippingConst?.units || 0) + nanoExceed;
+
+ return {
+ units: unitSum,
+ currencyCode: selectedCurrency,
+ nanos: nanoSum % 1000000000,
+ };
+ }, [shippingConst?.units, shippingConst?.nanos, productList, selectedCurrency]);
+
+ return (
+ <S.CartItems>
+ <S.CardItemsHeader>
+ <label>Product</label>
+ <label>Quantity</label>
+ <label>Price</label>
+ </S.CardItemsHeader>
+ {productList.map(({ productId, product, quantity }) => (
+ <CartItem key={productId} product={product} quantity={quantity} />
+ ))}
+ {shouldShowPrice && (
+ <>
+ <S.DataRow>
+ <span>Shipping</span>
+ <ProductPrice price={shippingConst} />
+ </S.DataRow>
+ <S.DataRow>
+ <S.TotalText>Total</S.TotalText>
+ <S.TotalText>
+ <ProductPrice price={total} />
+ </S.TotalText>
+ </S.DataRow>
+ </>
+ )}
+ </S.CartItems>
+ );
+};
+
+export default CartItems;
diff --git a/src/frontend/components/CartItems/index.ts b/src/frontend/components/CartItems/index.ts
new file mode 100644
index 0000000..ad419fa
--- /dev/null
+++ b/src/frontend/components/CartItems/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './CartItems';
diff --git a/src/frontend/components/CheckoutForm/CheckoutForm.styled.ts b/src/frontend/components/CheckoutForm/CheckoutForm.styled.ts
new file mode 100644
index 0000000..c11ebc5
--- /dev/null
+++ b/src/frontend/components/CheckoutForm/CheckoutForm.styled.ts
@@ -0,0 +1,59 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+import Button from '../Button';
+
+export const CheckoutForm = styled.form``;
+
+export const StateRow = styled.div`
+ display: grid;
+ grid-template-columns: 35% 55%;
+ gap: 10%;
+`;
+
+export const Title = styled.h1`
+ margin: 0;
+ margin-bottom: 24px;
+`;
+
+export const CardRow = styled.div`
+ display: grid;
+ grid-template-columns: 35% 35% 20%;
+ gap: 5%;
+`;
+
+export const SubmitContainer = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+ flex-direction: column-reverse;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ flex-direction: row;
+ justify-content: end;
+ align-items: center;
+ margin-top: 67px;
+ }
+`;
+
+export const CartButton = styled(Button)`
+ padding: 16px 35px;
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ width: 100%;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ width: inherit;
+ }
+`;
+
+export const EmptyCartButton = styled(Button)`
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ color: ${({ theme }) => theme.colors.otelRed};
+ width: 100%;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ width: inherit;
+ }
+`;
diff --git a/src/frontend/components/CheckoutForm/CheckoutForm.tsx b/src/frontend/components/CheckoutForm/CheckoutForm.tsx
new file mode 100644
index 0000000..1c8e5c9
--- /dev/null
+++ b/src/frontend/components/CheckoutForm/CheckoutForm.tsx
@@ -0,0 +1,202 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link';
+import { useCallback, useState } from 'react';
+import { CypressFields } from '../../utils/enums/CypressFields';
+import Input from '../Input';
+import * as S from './CheckoutForm.styled';
+
+const currentYear = new Date().getFullYear();
+const yearList = Array.from(new Array(20), (v, i) => i + currentYear);
+
+export interface IFormData {
+ email: string;
+ streetAddress: string;
+ city: string;
+ state: string;
+ country: string;
+ zipCode: string;
+ creditCardNumber: string;
+ creditCardCvv: number;
+ creditCardExpirationYear: number;
+ creditCardExpirationMonth: number;
+}
+
+interface IProps {
+ onSubmit(formData: IFormData): void;
+}
+
+const CheckoutForm = ({ onSubmit }: IProps) => {
+ const [
+ {
+ email,
+ streetAddress,
+ city,
+ state,
+ country,
+ zipCode,
+ creditCardCvv,
+ creditCardExpirationMonth,
+ creditCardExpirationYear,
+ creditCardNumber,
+ },
+ setFormData,
+ ] = useState<IFormData>({
+ email: 'someone@example.com',
+ streetAddress: '1600 Amphitheatre Parkway',
+ city: 'Mountain View',
+ state: 'CA',
+ country: 'United States',
+ zipCode: "94043",
+ creditCardNumber: '4432-8015-6152-0454',
+ creditCardCvv: 672,
+ creditCardExpirationYear: 2030,
+ creditCardExpirationMonth: 1,
+ });
+
+ const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
+ setFormData(formData => ({
+ ...formData,
+ [e.target.name]: e.target.value,
+ }));
+ }, []);
+
+ return (
+ <S.CheckoutForm
+ onSubmit={(event: { preventDefault: () => void; }) => {
+ event.preventDefault();
+ onSubmit({
+ email,
+ streetAddress,
+ city,
+ state,
+ country,
+ zipCode,
+ creditCardCvv,
+ creditCardExpirationMonth,
+ creditCardExpirationYear,
+ creditCardNumber,
+ });
+ }}
+ >
+ <S.Title>Shipping Address</S.Title>
+
+ <Input
+ label="E-mail Address"
+ type="email"
+ id="email"
+ name="email"
+ value={email}
+ required
+ onChange={handleChange}
+ />
+ <Input
+ label="Street Address"
+ type="text"
+ name="streetAddress"
+ id="street_address"
+ value={streetAddress}
+ onChange={handleChange}
+ required
+ />
+ <Input
+ label="Zip Code"
+ type="text"
+ name="zipCode"
+ id="zip_code"
+ value={zipCode}
+ onChange={handleChange}
+ required
+ />
+ <Input label="City" type="text" name="city" id="city" value={city} required onChange={handleChange} />
+
+ <S.StateRow>
+ <Input label="State" type="text" name="state" id="state" value={state} required onChange={handleChange} />
+ <Input
+ label="Country"
+ type="text"
+ id="country"
+ placeholder="Country Name"
+ name="country"
+ value={country}
+ onChange={handleChange}
+ required
+ />
+ </S.StateRow>
+
+ <div>
+ <S.Title>Payment Method</S.Title>
+ </div>
+
+ <Input
+ type="text"
+ label="Credit Card Number"
+ id="credit_card_number"
+ name="creditCardNumber"
+ placeholder="0000-0000-0000-0000"
+ value={creditCardNumber}
+ onChange={handleChange}
+ required
+ pattern="\d{4}-\d{4}-\d{4}-\d{4}"
+ />
+
+ <S.CardRow>
+ <Input
+ label="Month"
+ name="creditCardExpirationMonth"
+ id="credit_card_expiration_month"
+ value={creditCardExpirationMonth}
+ onChange={handleChange}
+ type="select"
+ >
+ <option value="1">January</option>
+ <option value="2">February</option>
+ <option value="3">March</option>
+ <option value="4">April</option>
+ <option value="5">May</option>
+ <option value="6">June</option>
+ <option value="7">July</option>
+ <option value="8">August</option>
+ <option value="9">September</option>
+ <option value="10">October</option>
+ <option value="11">November</option>
+ <option value="12">January</option>
+ </Input>
+ <Input
+ label="Year"
+ name="creditCardExpirationYear"
+ id="credit_card_expiration_year"
+ value={creditCardExpirationYear}
+ onChange={handleChange}
+ type="select"
+ >
+ {yearList.map(year => (
+ <option value={year} key={year}>
+ {year}
+ </option>
+ ))}
+ </Input>
+ <Input
+ label="CVV"
+ type="password"
+ id="credit_card_cvv"
+ name="creditCardCvv"
+ value={creditCardCvv}
+ required
+ pattern="\d{3}"
+ onChange={handleChange}
+ />
+ </S.CardRow>
+
+ <S.SubmitContainer>
+ <Link href="/">
+ <S.CartButton $type="secondary">Continue Shopping</S.CartButton>
+ </Link>
+ <S.CartButton data-cy={CypressFields.CheckoutPlaceOrder} type="submit">Place Order</S.CartButton>
+ </S.SubmitContainer>
+ </S.CheckoutForm>
+ );
+};
+
+export default CheckoutForm;
diff --git a/src/frontend/components/CheckoutForm/index.ts b/src/frontend/components/CheckoutForm/index.ts
new file mode 100644
index 0000000..897f43b
--- /dev/null
+++ b/src/frontend/components/CheckoutForm/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './CheckoutForm';
diff --git a/src/frontend/components/CheckoutItem/CheckoutItem.styled.ts b/src/frontend/components/CheckoutItem/CheckoutItem.styled.ts
new file mode 100644
index 0000000..013250e
--- /dev/null
+++ b/src/frontend/components/CheckoutItem/CheckoutItem.styled.ts
@@ -0,0 +1,90 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Image from 'next/image';
+import styled from 'styled-components';
+
+export const CheckoutItem = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ padding: 25px;
+ border-radius: 5px;
+ border: 1px solid ${({ theme }) => theme.colors.lightBorderGray};
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ grid-template-columns: 40% 40% 1fr;
+ }
+`;
+
+export const ItemDetails = styled.div`
+ display: flex;
+ gap: 25px;
+ padding-bottom: 25px;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.lightBorderGray};
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ padding-bottom: 0;
+ padding-right: 25px;
+ border-bottom: none;
+ border-right: 1px solid ${({ theme }) => theme.colors.lightBorderGray};
+ }
+`;
+
+export const Details = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+
+ span,
+ p {
+ margin: 0;
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ }
+`;
+
+export const ItemName = styled.h5`
+ margin: 0;
+ font-size: ${({ theme }) => theme.sizes.mLarge};
+`;
+
+export const ShippingData = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: 5px;
+ padding: 25px 0;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.lightBorderGray};
+
+ p {
+ margin: 0;
+ font-weight: ${({ theme }) => theme.fonts.light};
+ }
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ padding: 0 25px;
+ border-bottom: none;
+ border-right: 1px solid ${({ theme }) => theme.colors.lightBorderGray};
+ }
+`;
+
+export const Status = styled.div`
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding-top: 25px;
+ gap: 10px;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ padding-top: 0;
+ }
+`;
+
+export const ItemImage = styled(Image).attrs({
+ width: '80',
+ height: '80',
+})`
+ border-radius: 5px;
+`;
+
+export const SeeMore = styled.a`
+ color: ${({ theme }) => theme.colors.otelBlue};
+`;
diff --git a/src/frontend/components/CheckoutItem/CheckoutItem.tsx b/src/frontend/components/CheckoutItem/CheckoutItem.tsx
new file mode 100644
index 0000000..8fd0ecf
--- /dev/null
+++ b/src/frontend/components/CheckoutItem/CheckoutItem.tsx
@@ -0,0 +1,61 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Image from 'next/image';
+import { useState } from 'react';
+import { CypressFields } from '../../utils/enums/CypressFields';
+import { Address } from '../../protos/demo';
+import { IProductCheckoutItem } from '../../types/Cart';
+import ProductPrice from '../ProductPrice';
+import * as S from './CheckoutItem.styled';
+
+interface IProps {
+ checkoutItem: IProductCheckoutItem;
+ address: Address;
+}
+
+const CheckoutItem = ({
+ checkoutItem: {
+ item: {
+ quantity,
+ product: { picture, name },
+ },
+ cost = { currencyCode: 'USD', units: 0, nanos: 0 },
+ },
+ address: { streetAddress = '', city = '', state = '', zipCode = '', country = '' },
+}: IProps) => {
+ const [isCollapsed, setIsCollapsed] = useState(false);
+
+ return (
+ <S.CheckoutItem data-cy={CypressFields.CheckoutItem}>
+ <S.ItemDetails>
+ <S.ItemImage src={"/images/products/" + picture} alt={name}/>
+ <S.Details>
+ <S.ItemName>{name}</S.ItemName>
+ <p>Quantity: {quantity}</p>
+ <p>
+ Total: <ProductPrice price={cost} />
+ </p>
+ </S.Details>
+ </S.ItemDetails>
+ <S.ShippingData>
+ <S.ItemName>Shipping Data</S.ItemName>
+ <p>Street: {streetAddress}</p>
+ {!isCollapsed && <S.SeeMore onClick={() => setIsCollapsed(true)}>See More</S.SeeMore>}
+ {isCollapsed && (
+ <>
+ <p>City: {city}</p>
+ <p>State: {state}</p>
+ <p>Zip Code: {zipCode}</p>
+ <p>Country: {country}</p>
+ </>
+ )}
+ </S.ShippingData>
+ <S.Status>
+ <Image src="/icons/Check.svg" alt="check" height="14" width="16" /> <span>Done</span>
+ </S.Status>
+ </S.CheckoutItem>
+ );
+};
+
+export default CheckoutItem;
diff --git a/src/frontend/components/CheckoutItem/index.ts b/src/frontend/components/CheckoutItem/index.ts
new file mode 100644
index 0000000..f4c6b78
--- /dev/null
+++ b/src/frontend/components/CheckoutItem/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './CheckoutItem';
diff --git a/src/frontend/components/CurrencySwitcher/CurrencySwitcher.styled.ts b/src/frontend/components/CurrencySwitcher/CurrencySwitcher.styled.ts
new file mode 100644
index 0000000..6ea0669
--- /dev/null
+++ b/src/frontend/components/CurrencySwitcher/CurrencySwitcher.styled.ts
@@ -0,0 +1,65 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const CurrencySwitcher = styled.div`
+ display: flex;
+ justify-content: flex-end;
+`;
+
+export const Container = styled.div`
+ display: flex;
+ align-items: center;
+ position: relative;
+ margin-left: 40px;
+ color: #605f64;
+
+ &::-webkit-input-placeholder,
+ &::-moz-placeholder,
+ :-ms-input-placeholder,
+ :-moz-placeholder {
+ font-size: 12px;
+ color: #605f64;
+ }
+`;
+
+export const SelectedConcurrency = styled.span`
+ font-size: ${({ theme }) => theme.sizes.mLarge};
+ text-align: center;
+ font-weight: ${({ theme }) => theme.fonts.regular};
+
+ position: relative;
+ left: 35px;
+ width: 20px;
+ display: inline-block;
+`;
+
+export const Arrow = styled.img.attrs({
+ src: '/icons/Chevron.svg',
+ alt: 'arrow',
+})`
+ position: absolute;
+ right: 15px;
+ width: 12px;
+ height: 17px;
+`;
+
+export const Select = styled.select`
+ -webkit-appearance: none;
+ -webkit-border-radius: 0px;
+ font-size: ${({ theme }) => theme.sizes.mLarge};
+ cursor: pointer;
+
+ display: flex;
+ align-items: center;
+ background: transparent;
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ border: 1px solid ${({ theme }) => theme.colors.borderGray};
+ width: 130px;
+ height: 40px;
+ flex-shrink: 0;
+ padding: 1px 0 0 45px;
+ font-size: 16px;
+ border-radius: 10px;
+`;
diff --git a/src/frontend/components/CurrencySwitcher/CurrencySwitcher.tsx b/src/frontend/components/CurrencySwitcher/CurrencySwitcher.tsx
new file mode 100644
index 0000000..0e3b174
--- /dev/null
+++ b/src/frontend/components/CurrencySwitcher/CurrencySwitcher.tsx
@@ -0,0 +1,37 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { useMemo } from 'react';
+import getSymbolFromCurrency from 'currency-symbol-map';
+import { useCurrency } from '../../providers/Currency.provider';
+import * as S from './CurrencySwitcher.styled';
+import { CypressFields } from '../../utils/enums/CypressFields';
+
+const CurrencySwitcher = () => {
+ const { currencyCodeList, setSelectedCurrency, selectedCurrency } = useCurrency();
+
+ const currencySymbol = useMemo(() => getSymbolFromCurrency(selectedCurrency), [selectedCurrency]);
+
+ return (
+ <S.CurrencySwitcher>
+ <S.Container>
+ <S.SelectedConcurrency>{currencySymbol}</S.SelectedConcurrency>
+ <S.Select
+ name="currency_code"
+ onChange={(event: { target: { value: string; }; }) => setSelectedCurrency(event.target.value)}
+ value={selectedCurrency}
+ data-cy={CypressFields.CurrencySwitcher}
+ >
+ {currencyCodeList.map(currencyCode => (
+ <option key={currencyCode} value={currencyCode}>
+ {currencyCode}
+ </option>
+ ))}
+ </S.Select>
+ <S.Arrow />
+ </S.Container>
+ </S.CurrencySwitcher>
+ );
+};
+
+export default CurrencySwitcher;
diff --git a/src/frontend/components/CurrencySwitcher/index.ts b/src/frontend/components/CurrencySwitcher/index.ts
new file mode 100644
index 0000000..3a0af9c
--- /dev/null
+++ b/src/frontend/components/CurrencySwitcher/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './CurrencySwitcher';
diff --git a/src/frontend/components/Footer/Footer.styled.ts b/src/frontend/components/Footer/Footer.styled.ts
new file mode 100644
index 0000000..513039b
--- /dev/null
+++ b/src/frontend/components/Footer/Footer.styled.ts
@@ -0,0 +1,16 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const Footer = styled.footer`
+ position: relative;
+ padding: 65px 9%;
+ background-color: ${({ theme }) => theme.colors.otelGray};
+
+ * {
+ color: ${({ theme }) => theme.colors.white};
+ font-size: ${({ theme }) => theme.sizes.dSmall};
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ }
+`;
diff --git a/src/frontend/components/Footer/Footer.tsx b/src/frontend/components/Footer/Footer.tsx
new file mode 100644
index 0000000..8831271
--- /dev/null
+++ b/src/frontend/components/Footer/Footer.tsx
@@ -0,0 +1,37 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { useEffect, useState } from 'react';
+import * as S from './Footer.styled';
+import SessionGateway from '../../gateways/Session.gateway';
+import { CypressFields } from '../../utils/enums/CypressFields';
+import PlatformFlag from '../PlatformFlag';
+
+const currentYear = new Date().getFullYear();
+
+const { userId } = SessionGateway.getSession();
+
+const Footer = () => {
+ const [sessionId, setSessionId] = useState('');
+
+ useEffect(() => {
+ setSessionId(userId);
+ }, []);
+
+ return (
+ <S.Footer>
+ <div>
+ <p>This website is hosted for demo purpose only. It is not an actual shop.</p>
+ <p>
+ <span data-cy={CypressFields.SessionId}>session-id: {sessionId}</span>
+ </p>
+ </div>
+ <p>
+ @ {currentYear} OpenTelemetry (<a href="https://github.com/open-telemetry/opentelemetry-demo">Source Code</a>)
+ </p>
+ <PlatformFlag />
+ </S.Footer>
+ );
+};
+
+export default Footer;
diff --git a/src/frontend/components/Footer/index.ts b/src/frontend/components/Footer/index.ts
new file mode 100644
index 0000000..0961dac
--- /dev/null
+++ b/src/frontend/components/Footer/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './Footer';
diff --git a/src/frontend/components/Header/Header.styled.ts b/src/frontend/components/Header/Header.styled.ts
new file mode 100644
index 0000000..39d6240
--- /dev/null
+++ b/src/frontend/components/Header/Header.styled.ts
@@ -0,0 +1,55 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Link from 'next/link';
+import styled from 'styled-components';
+
+export const Header = styled.header`
+ background-color: #853b5c;
+ color: white;
+`;
+
+export const NavBar = styled.nav`
+ height: 80px;
+ background-color: white;
+ font-size: 15px;
+ color: #b4b2bb;
+ border-bottom: 1px solid ${({ theme }) => theme.colors.textGray};
+ z-index: 1;
+ padding: 0;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ height: 100px;
+ }
+`;
+
+export const Container = styled.div`
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ padding: 0 20px;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ padding: 25px 100px;
+ }
+`;
+
+export const NavBarBrand = styled(Link)`
+ display: flex;
+ align-items: center;
+ padding: 0;
+`;
+
+export const BrandImg = styled.img.attrs({
+ src: '/images/opentelemetry-demo-logo.png',
+})`
+ width: 280px;
+ height: auto;
+`;
+
+export const Controls = styled.div`
+ display: flex;
+ height: 60px;
+`;
diff --git a/src/frontend/components/Header/Header.tsx b/src/frontend/components/Header/Header.tsx
new file mode 100644
index 0000000..857fc7b
--- /dev/null
+++ b/src/frontend/components/Header/Header.tsx
@@ -0,0 +1,26 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import CartIcon from '../CartIcon';
+import CurrencySwitcher from '../CurrencySwitcher';
+import * as S from './Header.styled';
+
+const Header = () => {
+ return (
+ <S.Header>
+ <S.NavBar>
+ <S.Container>
+ <S.NavBarBrand href="/">
+ <S.BrandImg />
+ </S.NavBarBrand>
+ <S.Controls>
+ <CurrencySwitcher />
+ <CartIcon />
+ </S.Controls>
+ </S.Container>
+ </S.NavBar>
+ </S.Header>
+ );
+};
+
+export default Header;
diff --git a/src/frontend/components/Header/index.ts b/src/frontend/components/Header/index.ts
new file mode 100644
index 0000000..4afb2c7
--- /dev/null
+++ b/src/frontend/components/Header/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export {default} from './Header';
diff --git a/src/frontend/components/Input/Input.styled.ts b/src/frontend/components/Input/Input.styled.ts
new file mode 100644
index 0000000..d2078d1
--- /dev/null
+++ b/src/frontend/components/Input/Input.styled.ts
@@ -0,0 +1,54 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const Input = styled.input`
+ width: -webkit-fill-available;
+ border: none;
+ padding: 16px;
+ outline: none;
+
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ font-size: ${({ theme }) => theme.sizes.dMedium};
+
+ border-radius: 10px;
+ background: #f9f9f9;
+ border: 1px solid #cacaca;
+`;
+
+export const InputLabel = styled.p`
+ font-size: ${({ theme }) => theme.sizes.dMedium};
+ font-weight: ${({ theme }) => theme.fonts.semiBold};
+ margin: 0;
+ margin-bottom: 15px;
+`;
+
+export const Select = styled.select`
+ width: 100%;
+ border: none;
+
+ padding: 16px;
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ font-size: ${({ theme }) => theme.sizes.dMedium};
+
+ border-radius: 10px;
+ background: #f9f9f9;
+ border: 1px solid #cacaca;
+`;
+
+export const InputRow = styled.div`
+ position: relative;
+ margin-bottom: 24px;
+`;
+
+export const Arrow = styled.img.attrs({
+ src: '/icons/Chevron.svg',
+ alt: 'arrow',
+})`
+ position: absolute;
+ right: 20px;
+ width: 10px;
+ height: 5px;
+ top: 64px;
+`;
diff --git a/src/frontend/components/Input/Input.tsx b/src/frontend/components/Input/Input.tsx
new file mode 100644
index 0000000..14782b0
--- /dev/null
+++ b/src/frontend/components/Input/Input.tsx
@@ -0,0 +1,31 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { HTMLInputTypeAttribute, InputHTMLAttributes } from 'react';
+import * as S from './Input.styled';
+
+interface IProps extends InputHTMLAttributes<HTMLSelectElement | HTMLInputElement> {
+ type: HTMLInputTypeAttribute | 'select';
+ children?: React.ReactNode;
+ label: string;
+}
+
+const Input = ({ type, id = '', children, label, ...props }: IProps) => {
+ return (
+ <S.InputRow>
+ <S.InputLabel>{label}</S.InputLabel>
+ {type === 'select' ? (
+ <>
+ <S.Select id={id} {...props}>
+ {children}
+ </S.Select>
+ <S.Arrow />
+ </>
+ ) : (
+ <S.Input id={id} {...props} type={type} />
+ )}
+ </S.InputRow>
+ );
+};
+
+export default Input;
diff --git a/src/frontend/components/Input/index.ts b/src/frontend/components/Input/index.ts
new file mode 100644
index 0000000..f1d0923
--- /dev/null
+++ b/src/frontend/components/Input/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export {default} from './Input';
diff --git a/src/frontend/components/Layout/Layout.tsx b/src/frontend/components/Layout/Layout.tsx
new file mode 100644
index 0000000..f4e2da8
--- /dev/null
+++ b/src/frontend/components/Layout/Layout.tsx
@@ -0,0 +1,19 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import Header from '../Header';
+
+interface IProps {
+ children: React.ReactNode;
+}
+
+const Layout = ({ children }: IProps) => {
+ return (
+ <>
+ <Header />
+ <main>{children}</main>
+ </>
+ );
+};
+
+export default Layout;
diff --git a/src/frontend/components/Layout/index.ts b/src/frontend/components/Layout/index.ts
new file mode 100644
index 0000000..8812dad
--- /dev/null
+++ b/src/frontend/components/Layout/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export {default} from './Layout';
diff --git a/src/frontend/components/PlatformFlag/PlatformFlag.styled.ts b/src/frontend/components/PlatformFlag/PlatformFlag.styled.ts
new file mode 100644
index 0000000..3ac0733
--- /dev/null
+++ b/src/frontend/components/PlatformFlag/PlatformFlag.styled.ts
@@ -0,0 +1,25 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const Block = styled.div`
+ position: absolute;
+ bottom: 0;
+ right: 0;
+ width: 100px;
+ height: 27px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ font-size: ${({ theme }) => theme.sizes.mSmall};
+ font-weight: ${({ theme }) => theme.fonts.regular};
+ color: ${({ theme }) => theme.colors.white};
+ background: ${({ theme }) => theme.colors.otelYellow};
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ width: 190px;
+ height: 50px;
+ font-size: ${({ theme }) => theme.sizes.dSmall};
+ }
+`;
diff --git a/src/frontend/components/PlatformFlag/PlatformFlag.tsx b/src/frontend/components/PlatformFlag/PlatformFlag.tsx
new file mode 100644
index 0000000..80a01ca
--- /dev/null
+++ b/src/frontend/components/PlatformFlag/PlatformFlag.tsx
@@ -0,0 +1,16 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import * as S from './PlatformFlag.styled';
+
+const { NEXT_PUBLIC_PLATFORM = 'local' } = typeof window !== 'undefined' ? window.ENV : {};
+
+const platform = NEXT_PUBLIC_PLATFORM;
+
+const PlatformFlag = () => {
+ return (
+ <S.Block>{platform}</S.Block>
+ );
+};
+
+export default PlatformFlag;
diff --git a/src/frontend/components/PlatformFlag/index.ts b/src/frontend/components/PlatformFlag/index.ts
new file mode 100644
index 0000000..ea744bb
--- /dev/null
+++ b/src/frontend/components/PlatformFlag/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './PlatformFlag';
diff --git a/src/frontend/components/ProductCard/ProductCard.styled.ts b/src/frontend/components/ProductCard/ProductCard.styled.ts
new file mode 100644
index 0000000..50d2f7a
--- /dev/null
+++ b/src/frontend/components/ProductCard/ProductCard.styled.ts
@@ -0,0 +1,36 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+import RouterLink from 'next/link';
+
+export const Link = styled(RouterLink)`
+ text-decoration: none;
+`;
+
+export const Image = styled.div<{ $src: string }>`
+ width: 100%;
+ height: 150px;
+ background: url(${({ $src }) => $src}) no-repeat center;
+ background-size: 100% auto;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ height: 300px;
+ }
+`;
+
+export const ProductCard = styled.div`
+ cursor: pointer;
+`;
+
+export const ProductName = styled.p`
+ margin: 0;
+ margin-top: 10px;
+ font-size: ${({ theme }) => theme.sizes.dSmall};
+`;
+
+export const ProductPrice = styled.p`
+ margin: 0;
+ font-size: ${({ theme }) => theme.sizes.dMedium};
+ font-weight: ${({ theme }) => theme.fonts.bold};
+`;
diff --git a/src/frontend/components/ProductCard/ProductCard.tsx b/src/frontend/components/ProductCard/ProductCard.tsx
new file mode 100644
index 0000000..0744e21
--- /dev/null
+++ b/src/frontend/components/ProductCard/ProductCard.tsx
@@ -0,0 +1,65 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { CypressFields } from '../../utils/enums/CypressFields';
+import { Product } from '../../protos/demo';
+import ProductPrice from '../ProductPrice';
+import * as S from './ProductCard.styled';
+import { useState, useEffect } from 'react';
+import { useNumberFlagValue } from '@openfeature/react-sdk';
+
+interface IProps {
+ product: Product;
+}
+
+async function getImageWithHeaders(requestInfo: Request) {
+ const res = await fetch(requestInfo);
+ return await res.blob();
+}
+
+const ProductCard = ({
+ product: {
+ id,
+ picture,
+ name,
+ priceUsd = {
+ currencyCode: 'USD',
+ units: 0,
+ nanos: 0,
+ },
+ },
+}: IProps) => {
+ const imageSlowLoad = useNumberFlagValue('imageSlowLoad', 0);
+ const [imageSrc, setImageSrc] = useState<string>('');
+
+ useEffect(() => {
+ const headers = new Headers();
+ headers.append('x-envoy-fault-delay-request', imageSlowLoad.toString());
+ headers.append('Cache-Control', 'no-cache')
+ const requestInit = {
+ method: "GET",
+ headers: headers
+ };
+ const image_url ='/images/products/' + picture
+ const requestInfo = new Request(image_url, requestInit);
+ getImageWithHeaders(requestInfo).then(blob => {
+ setImageSrc(URL.createObjectURL(blob));
+ });
+ }, [imageSlowLoad, picture]);
+
+ return (
+ <S.Link href={`/product/${id}`}>
+ <S.ProductCard data-cy={CypressFields.ProductCard}>
+ <S.Image $src={imageSrc} />
+ <div>
+ <S.ProductName>{name}</S.ProductName>
+ <S.ProductPrice>
+ <ProductPrice price={priceUsd} />
+ </S.ProductPrice>
+ </div>
+ </S.ProductCard>
+ </S.Link>
+ );
+};
+
+export default ProductCard;
diff --git a/src/frontend/components/ProductCard/index.ts b/src/frontend/components/ProductCard/index.ts
new file mode 100644
index 0000000..ba9de7a
--- /dev/null
+++ b/src/frontend/components/ProductCard/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './ProductCard';
diff --git a/src/frontend/components/ProductList/ProductList.styled.ts b/src/frontend/components/ProductList/ProductList.styled.ts
new file mode 100644
index 0000000..3776404
--- /dev/null
+++ b/src/frontend/components/ProductList/ProductList.styled.ts
@@ -0,0 +1,14 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const ProductList = styled.div`
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 24px;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ grid-template-columns: repeat(3, auto);
+ }
+`;
diff --git a/src/frontend/components/ProductList/ProductList.tsx b/src/frontend/components/ProductList/ProductList.tsx
new file mode 100644
index 0000000..0546eae
--- /dev/null
+++ b/src/frontend/components/ProductList/ProductList.tsx
@@ -0,0 +1,23 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { CypressFields } from '../../utils/enums/CypressFields';
+import { Product } from '../../protos/demo';
+import ProductCard from '../ProductCard';
+import * as S from './ProductList.styled';
+
+interface IProps {
+ productList: Product[];
+}
+
+const ProductList = ({ productList }: IProps) => {
+ return (
+ <S.ProductList data-cy={CypressFields.ProductList}>
+ {productList.map(product => (
+ <ProductCard key={product.id} product={product} />
+ ))}
+ </S.ProductList>
+ );
+};
+
+export default ProductList;
diff --git a/src/frontend/components/ProductList/index.ts b/src/frontend/components/ProductList/index.ts
new file mode 100644
index 0000000..3ff921c
--- /dev/null
+++ b/src/frontend/components/ProductList/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './ProductList';
diff --git a/src/frontend/components/ProductPrice/ProductPrice.tsx b/src/frontend/components/ProductPrice/ProductPrice.tsx
new file mode 100644
index 0000000..8dc352a
--- /dev/null
+++ b/src/frontend/components/ProductPrice/ProductPrice.tsx
@@ -0,0 +1,31 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { useMemo } from 'react';
+import getSymbolFromCurrency from 'currency-symbol-map';
+import { Money } from '../../protos/demo';
+import { useCurrency } from '../../providers/Currency.provider';
+import { CypressFields } from '../../utils/enums/CypressFields';
+
+interface IProps {
+ price: Money;
+}
+
+const ProductPrice = ({ price: { units, currencyCode, nanos } }: IProps) => {
+ const { selectedCurrency } = useCurrency();
+
+ const currencySymbol = useMemo(
+ () => getSymbolFromCurrency(currencyCode) || selectedCurrency,
+ [currencyCode, selectedCurrency]
+ );
+
+ const total = units + nanos / 1000000000;
+
+ return (
+ <span data-cy={CypressFields.ProductPrice}>
+ {currencySymbol} {total.toFixed(2)}
+ </span>
+ );
+};
+
+export default ProductPrice;
diff --git a/src/frontend/components/ProductPrice/index.ts b/src/frontend/components/ProductPrice/index.ts
new file mode 100644
index 0000000..7bbda4a
--- /dev/null
+++ b/src/frontend/components/ProductPrice/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './ProductPrice';
diff --git a/src/frontend/components/Recommendations/Recommendations.styled.ts b/src/frontend/components/Recommendations/Recommendations.styled.ts
new file mode 100644
index 0000000..1667be4
--- /dev/null
+++ b/src/frontend/components/Recommendations/Recommendations.styled.ts
@@ -0,0 +1,39 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const Recommendations = styled.section`
+ display: flex;
+ margin: 40px 0;
+ align-items: center;
+ flex-direction: column;
+`;
+
+export const ProductList = styled.div`
+ display: flex;
+ width: 100%;
+ padding: 0 20px;
+ flex-direction: column;
+ gap: 24px;
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+ }
+`;
+
+export const TitleContainer = styled.div`
+ border-top: 1px dashed;
+ padding: 40px 0;
+ text-align: center;
+ width: 100%;
+`;
+
+export const Title = styled.h3`
+ font-size: ${({ theme }) => theme.sizes.mLarge};
+
+ ${({ theme }) => theme.breakpoints.desktop} {
+ font-size: ${({ theme }) => theme.sizes.dLarge};
+ }
+`;
diff --git a/src/frontend/components/Recommendations/Recommendations.tsx b/src/frontend/components/Recommendations/Recommendations.tsx
new file mode 100644
index 0000000..3a5d64f
--- /dev/null
+++ b/src/frontend/components/Recommendations/Recommendations.tsx
@@ -0,0 +1,26 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { CypressFields } from '../../utils/enums/CypressFields';
+import { useAd } from '../../providers/Ad.provider';
+import ProductCard from '../ProductCard';
+import * as S from './Recommendations.styled';
+
+const Recommendations = () => {
+ const { recommendedProductList } = useAd();
+
+ return (
+ <S.Recommendations data-cy={CypressFields.RecommendationList}>
+ <S.TitleContainer>
+ <S.Title>You May Also Like</S.Title>
+ </S.TitleContainer>
+ <S.ProductList>
+ {recommendedProductList.map(product => (
+ <ProductCard key={product.id} product={product} />
+ ))}
+ </S.ProductList>
+ </S.Recommendations>
+ );
+};
+
+export default Recommendations;
diff --git a/src/frontend/components/Recommendations/index.ts b/src/frontend/components/Recommendations/index.ts
new file mode 100644
index 0000000..625afa5
--- /dev/null
+++ b/src/frontend/components/Recommendations/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './Recommendations';
diff --git a/src/frontend/components/Select/Select.styled.ts b/src/frontend/components/Select/Select.styled.ts
new file mode 100644
index 0000000..319ee59
--- /dev/null
+++ b/src/frontend/components/Select/Select.styled.ts
@@ -0,0 +1,31 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import styled from 'styled-components';
+
+export const Select = styled.select`
+ width: 100%;
+ height: 45px;
+ border: 1px solid ${({ theme }) => theme.colors.borderGray};
+ padding: 10px 16px;
+ border-radius: 8px;
+ position: relative;
+ width: 100px;
+ cursor: pointer;
+`;
+
+export const SelectContainer = styled.div`
+ position: relative;
+ width: min-content;
+`;
+
+export const Arrow = styled.img.attrs({
+ src: '/icons/Chevron.svg',
+ alt: 'select',
+})`
+ position: absolute;
+ right: 25px;
+ top: 20px;
+ width: 10px;
+ height: 5px;
+`;
diff --git a/src/frontend/components/Select/Select.tsx b/src/frontend/components/Select/Select.tsx
new file mode 100644
index 0000000..a1f7e2a
--- /dev/null
+++ b/src/frontend/components/Select/Select.tsx
@@ -0,0 +1,20 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+import { InputHTMLAttributes } from 'react';
+import * as S from './Select.styled';
+
+interface IProps extends InputHTMLAttributes<HTMLSelectElement> {
+ children: React.ReactNode;
+}
+
+const Select = ({ children, ...props }: IProps) => {
+ return (
+ <S.SelectContainer>
+ <S.Select {...props}>{children}</S.Select>
+ <S.Arrow />
+ </S.SelectContainer>
+ );
+};
+
+export default Select;
diff --git a/src/frontend/components/Select/index.ts b/src/frontend/components/Select/index.ts
new file mode 100644
index 0000000..2f51fcd
--- /dev/null
+++ b/src/frontend/components/Select/index.ts
@@ -0,0 +1,4 @@
+// Copyright The OpenTelemetry Authors
+// SPDX-License-Identifier: Apache-2.0
+
+export { default } from './Select';