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/components | |
Initial snapshot - OpenTelemetry demo 2.1.3 -f
Diffstat (limited to 'src/frontend/components')
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'; |
