From f1fa266ea34d3ef5cfea9571270a1d080b323268 Mon Sep 17 00:00:00 2001 From: Juliano Costa Date: Thu, 16 Oct 2025 16:08:42 +0200 Subject: [PATCH 1/3] [frontend] Fix navigation and cart calculation --- .../CartDropdown/CartDropdown.styled.ts | 1 + .../components/CartItems/CartItems.styled.ts | 1 + .../components/CartItems/CartItems.tsx | 4 +- .../CheckoutItem/CheckoutItem.styled.ts | 1 + .../ProductCard/ProductCard.styled.ts | 2 +- .../pages/cart/checkout/[orderId]/index.tsx | 95 +++++++-- src/frontend/styles/Checkout.styled.ts | 189 ++++++++++++++++-- src/frontend/styles/ProductDetail.styled.ts | 2 +- 8 files changed, 261 insertions(+), 34 deletions(-) diff --git a/src/frontend/components/CartDropdown/CartDropdown.styled.ts b/src/frontend/components/CartDropdown/CartDropdown.styled.ts index 882021090b..0da5f94c51 100644 --- a/src/frontend/components/CartDropdown/CartDropdown.styled.ts +++ b/src/frontend/components/CartDropdown/CartDropdown.styled.ts @@ -60,6 +60,7 @@ export const ItemImage = styled(Image).attrs({ height: '80', })` border-radius: 5px; + object-fit: contain; `; export const ItemName = styled.p` diff --git a/src/frontend/components/CartItems/CartItems.styled.ts b/src/frontend/components/CartItems/CartItems.styled.ts index a1a5fbcf18..884d675832 100644 --- a/src/frontend/components/CartItems/CartItems.styled.ts +++ b/src/frontend/components/CartItems/CartItems.styled.ts @@ -22,6 +22,7 @@ export const CartItemImage = styled.img` width: 100%; height: auto; border-radius: 5px; + object-fit: contain; ${({ theme }) => theme.breakpoints.desktop} { width: 120px; diff --git a/src/frontend/components/CartItems/CartItems.tsx b/src/frontend/components/CartItems/CartItems.tsx index 05e0279ec2..11350d392d 100644 --- a/src/frontend/components/CartItems/CartItems.tsx +++ b/src/frontend/components/CartItems/CartItems.tsx @@ -36,12 +36,12 @@ const CartItems = ({ productList, shouldShowPrice = true }: IProps) => { const total = useMemo(() => { const nanoSum = - productList.reduce((acc, { product: { priceUsd: { nanos = 0 } = {} } }) => acc + Number(nanos), 0) + + productList.reduce((acc, { product: { priceUsd: { nanos = 0 } = {} }, quantity }) => acc + Number(nanos) * quantity, 0) + shippingConst?.nanos || 0; const nanoExceed = Math.floor(nanoSum / 1000000000); const unitSum = - productList.reduce((acc, { product: { priceUsd: { units = 0 } = {} } }) => acc + Number(units), 0) + + productList.reduce((acc, { product: { priceUsd: { units = 0 } = {} }, quantity }) => acc + Number(units) * quantity, 0) + (shippingConst?.units || 0) + nanoExceed; return { diff --git a/src/frontend/components/CheckoutItem/CheckoutItem.styled.ts b/src/frontend/components/CheckoutItem/CheckoutItem.styled.ts index 013250ebad..1658215061 100644 --- a/src/frontend/components/CheckoutItem/CheckoutItem.styled.ts +++ b/src/frontend/components/CheckoutItem/CheckoutItem.styled.ts @@ -83,6 +83,7 @@ export const ItemImage = styled(Image).attrs({ height: '80', })` border-radius: 5px; + object-fit: contain; `; export const SeeMore = styled.a` diff --git a/src/frontend/components/ProductCard/ProductCard.styled.ts b/src/frontend/components/ProductCard/ProductCard.styled.ts index 50d2f7a586..a6f7018876 100644 --- a/src/frontend/components/ProductCard/ProductCard.styled.ts +++ b/src/frontend/components/ProductCard/ProductCard.styled.ts @@ -12,7 +12,7 @@ export const Image = styled.div<{ $src: string }>` width: 100%; height: 150px; background: url(${({ $src }) => $src}) no-repeat center; - background-size: 100% auto; + background-size: contain; ${({ theme }) => theme.breakpoints.desktop} { height: 300px; diff --git a/src/frontend/pages/cart/checkout/[orderId]/index.tsx b/src/frontend/pages/cart/checkout/[orderId]/index.tsx index 740b895307..3071973a2e 100644 --- a/src/frontend/pages/cart/checkout/[orderId]/index.tsx +++ b/src/frontend/pages/cart/checkout/[orderId]/index.tsx @@ -5,19 +5,40 @@ import { NextPage } from 'next'; import Head from 'next/head'; import Link from 'next/link'; import { useRouter } from 'next/router'; +import { useMemo } from 'react'; import Ad from '../../../../components/Ad'; import Button from '../../../../components/Button'; -import CheckoutItem from '../../../../components/CheckoutItem'; import Footer from '../../../../components/Footer'; import Layout from '../../../../components/Layout'; +import ProductPrice from '../../../../components/ProductPrice'; import Recommendations from '../../../../components/Recommendations'; import AdProvider from '../../../../providers/Ad.provider'; +import { Money } from '../../../../protos/demo'; import * as S from '../../../../styles/Checkout.styled'; import { IProductCheckout } from '../../../../types/Cart'; const Checkout: NextPage = () => { const { query } = useRouter(); - const { items = [], shippingAddress } = JSON.parse((query.order || '{}') as string) as IProductCheckout; + const { orderId, items = [], shippingAddress, shippingCost = { units: 0, currencyCode: 'USD', nanos: 0 } } = JSON.parse((query.order || '{}') as string) as IProductCheckout; + + const orderTotal = useMemo(() => { + const itemsTotal = items.reduce((acc, { cost = { units: 0, nanos: 0, currencyCode: 'USD' } }) => { + return { + units: acc.units + (cost.units || 0), + nanos: acc.nanos + (cost.nanos || 0), + currencyCode: cost.currencyCode || 'USD', + }; + }, { units: 0, nanos: 0, currencyCode: 'USD' }); + + const totalNanos = itemsTotal.nanos + (shippingCost.nanos || 0); + const nanoExceed = Math.floor(totalNanos / 1000000000); + + return { + units: itemsTotal.units + (shippingCost.units || 0) + nanoExceed, + nanos: totalNanos % 1000000000, + currencyCode: shippingCost.currencyCode || 'USD', + }; + }, [items, shippingCost]); return ( { - Your order is complete! - We've sent you a confirmation email. - - - {items.map(checkoutItem => ( - - ))} - + + Your order is complete! + We've sent you a confirmation email. + + Order ID: + {orderId} + + + + + Shipping Address + {shippingAddress.streetAddress} + {shippingAddress.city}, {shippingAddress.state} {shippingAddress.zipCode} + {shippingAddress.country} + + + + Order Items + + {items.map(({ item, cost = { units: 0, currencyCode: 'USD', nanos: 0 } }) => { + const itemTotal: Money = { + units: (cost.units || 0) * item.quantity, + nanos: (cost.nanos || 0) * item.quantity, + currencyCode: cost.currencyCode || 'USD', + }; + // Handle nanos overflow + const nanoExceed = Math.floor(itemTotal.nanos / 1000000000); + itemTotal.units += nanoExceed; + itemTotal.nanos = itemTotal.nanos % 1000000000; + + return ( + + + + {item.product.name} + Quantity: {item.quantity} + + + + + + ); + })} + + + + + Shipping: + + + + Total: + + + + + + diff --git a/src/frontend/styles/Checkout.styled.ts b/src/frontend/styles/Checkout.styled.ts index 69e340f80d..629ce1b3c4 100644 --- a/src/frontend/styles/Checkout.styled.ts +++ b/src/frontend/styles/Checkout.styled.ts @@ -15,47 +15,70 @@ export const Container = styled.div` display: flex; flex-direction: column; gap: 28px; - align-items: center; - justify-content: center; margin-bottom: 120px; ${({ theme }) => theme.breakpoints.desktop} { display: grid; - grid-template-columns: auto; + grid-template-columns: 1fr 1fr; + grid-template-areas: + "left right" + "items items" + "button button"; + gap: 40px; } `; -export const DataRow = styled.div` - display: grid; - width: 100%; - justify-content: space-between; - grid-template-columns: 1fr 1fr; - padding: 24px 0; - border-top: solid 1px rgba(154, 160, 166, 0.5); +export const LeftColumn = styled.div` + display: flex; + flex-direction: column; + gap: 16px; - span:last-of-type { + ${({ theme }) => theme.breakpoints.desktop} { + grid-area: left; + } +`; + +export const SectionTitle = styled.h4` + margin: 0 0 12px 0; + font-size: ${({ theme }) => theme.sizes.mLarge}; + color: ${({ theme }) => theme.colors.textGray}; + text-align: left; +`; + +export const RightColumn = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + padding: 20px; + background-color: ${({ theme }) => theme.colors.backgroundGray}; + border-radius: 8px; + + ${({ theme }) => theme.breakpoints.desktop} { + grid-area: right; + } + + ${SectionTitle} { text-align: right; } `; -export const ItemList = styled.div` - width: 100%; +export const ItemsSection = styled.div` display: flex; flex-direction: column; gap: 24px; ${({ theme }) => theme.breakpoints.desktop} { - margin: 72px 0; + grid-area: items; } `; export const Title = styled.h1` text-align: center; margin: 0; - font-size: ${({ theme }) => theme.sizes.mLarge}; ${({ theme }) => theme.breakpoints.desktop} { + text-align: left; font-size: ${({ theme }) => theme.sizes.dLarge}; } `; @@ -63,17 +86,151 @@ export const Title = styled.h1` export const Subtitle = styled.h3` text-align: center; margin: 0; - font-size: ${({ theme }) => theme.sizes.mMedium}; color: ${({ theme }) => theme.colors.textLightGray}; ${({ theme }) => theme.breakpoints.desktop} { + text-align: left; font-size: ${({ theme }) => theme.sizes.dMedium}; } `; +export const OrderInfo = styled.div` + display: flex; + gap: 8px; + justify-content: center; + margin-top: 8px; + + ${({ theme }) => theme.breakpoints.desktop} { + justify-content: flex-start; + } +`; + +export const InfoLabel = styled.span` + font-weight: ${({ theme }) => theme.fonts.bold}; + color: ${({ theme }) => theme.colors.textGray}; +`; + +export const InfoValue = styled.span` + color: ${({ theme }) => theme.colors.textGray}; +`; + +export const AddressText = styled.p` + margin: 4px 0; + font-size: ${({ theme }) => theme.sizes.mMedium}; + color: ${({ theme }) => theme.colors.textGray}; + text-align: right; +`; + +export const ItemList = styled.div` + display: flex; + flex-direction: column; + gap: 16px; +`; + +export const OrderItem = styled.div` + display: flex; + gap: 16px; + align-items: center; + padding: 16px; + background-color: ${({ theme }) => theme.colors.white}; + border: 1px solid ${({ theme }) => theme.colors.lightBorderGray}; + border-radius: 8px; +`; + +export const ItemImage = styled.img` + width: 80px; + height: 80px; + object-fit: contain; + border-radius: 5px; + flex-shrink: 0; +`; + +export const ItemDetails = styled.div` + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +`; + +export const ItemName = styled.h5` + margin: 0; + font-size: ${({ theme }) => theme.sizes.mLarge}; + font-weight: ${({ theme }) => theme.fonts.regular}; +`; + +export const ItemQuantity = styled.p` + margin: 0; + font-size: ${({ theme }) => theme.sizes.mMedium}; + color: ${({ theme }) => theme.colors.textLightGray}; +`; + +export const ItemPrice = styled.div` + font-size: ${({ theme }) => theme.sizes.mLarge}; + font-weight: ${({ theme }) => theme.fonts.bold}; + color: ${({ theme }) => theme.colors.textGray}; + text-align: right; + white-space: nowrap; +`; + +export const OrderSummary = styled.div` + display: flex; + flex-direction: column; + gap: 12px; + padding: 24px; + background-color: ${({ theme }) => theme.colors.backgroundGray}; + border-radius: 8px; + margin-top: 16px; +`; + +export const SummaryRow = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + font-size: ${({ theme }) => theme.sizes.mMedium}; + color: ${({ theme }) => theme.colors.textGray}; +`; + +export const TotalRow = styled.div` + display: flex; + justify-content: space-between; + align-items: center; + padding-top: 12px; + border-top: 2px solid ${({ theme }) => theme.colors.borderGray}; + margin-top: 8px; +`; + +export const TotalLabel = styled.span` + font-size: ${({ theme }) => theme.sizes.mLarge}; + font-weight: ${({ theme }) => theme.fonts.bold}; + color: ${({ theme }) => theme.colors.textGray}; +`; + +export const TotalAmount = styled.span` + font-size: ${({ theme }) => theme.sizes.mLarge}; + font-weight: ${({ theme }) => theme.fonts.bold}; + color: ${({ theme }) => theme.colors.textGray}; +`; + export const ButtonContainer = styled.div` display: flex; justify-content: center; align-items: center; + + ${({ theme }) => theme.breakpoints.desktop} { + grid-area: button; + } +`; + +export const DataRow = styled.div` + display: grid; + width: 100%; + justify-content: space-between; + grid-template-columns: 1fr 1fr; + padding: 10px 0; + border-top: solid 1px rgba(154, 160, 166, 0.5); + + span:last-of-type { + text-align: right; + } `; diff --git a/src/frontend/styles/ProductDetail.styled.ts b/src/frontend/styles/ProductDetail.styled.ts index a2dda3839c..b919c45a1b 100644 --- a/src/frontend/styles/ProductDetail.styled.ts +++ b/src/frontend/styles/ProductDetail.styled.ts @@ -25,7 +25,7 @@ export const Image = styled.div<{ $src: string }>` height: 150px; background: url(${({ $src }) => $src}) no-repeat center; - background-size: 100% auto; + background-size: contain; ${({ theme }) => theme.breakpoints.desktop} { height: 500px; From 423f5e93fee487833c788e895a44a230dd4c86b3 Mon Sep 17 00:00:00 2001 From: Juliano Costa Date: Fri, 17 Oct 2025 11:08:06 +0200 Subject: [PATCH 2/3] Fix footer and cart alignment --- .../CartDropdown/CartDropdown.styled.ts | 30 +++++++++++++++---- .../components/CartDropdown/CartDropdown.tsx | 4 +-- src/frontend/components/Layout/Layout.tsx | 2 ++ .../Recommendations/Recommendations.tsx | 4 +++ .../pages/cart/checkout/[orderId]/index.tsx | 2 -- src/frontend/pages/cart/index.tsx | 2 -- src/frontend/pages/index.tsx | 2 -- .../pages/product/[productId]/index.tsx | 2 -- src/frontend/styles/globals.css | 14 +++++++++ 9 files changed, 47 insertions(+), 15 deletions(-) diff --git a/src/frontend/components/CartDropdown/CartDropdown.styled.ts b/src/frontend/components/CartDropdown/CartDropdown.styled.ts index 0da5f94c51..d4318f16e0 100644 --- a/src/frontend/components/CartDropdown/CartDropdown.styled.ts +++ b/src/frontend/components/CartDropdown/CartDropdown.styled.ts @@ -12,10 +12,10 @@ export const CartDropdown = styled.div` width: 100%; height: 100%; max-height: 100%; - padding: 25px; + padding: 5px; display: flex; flex-direction: column; - justify-content: space-between; + align-items: center; gap: 24px; background: ${({ theme }) => theme.colors.white}; z-index: 1000; @@ -27,7 +27,7 @@ export const CartDropdown = styled.div` width: 400px; top: 95px; right: 17px; - max-height: 650px; + max-height: 600px; } `; @@ -43,7 +43,7 @@ export const Title = styled.h5` export const ItemList = styled.div` ${({ theme }) => theme.breakpoints.desktop} { max-height: 450px; - overflow-y: scroll; + overflow-y: auto; } `; @@ -81,10 +81,29 @@ export const ItemQuantity = styled(ItemName)` export const CartButton = styled(Button)``; +export const ContentWrapper = styled.div` + width: 100%; + overflow-y: auto; + flex: 1; + min-height: 0; + + ${({ theme }) => theme.breakpoints.desktop} { + overflow-y: visible; + flex: 0 1 auto; + min-height: auto; + } +`; + export const Header = styled.div` display: flex; - justify-content: space-between; + justify-content: center; align-items: center; + width: 100%; + + span { + position: absolute; + right: 25px; + } ${({ theme }) => theme.breakpoints.desktop} { span { @@ -98,4 +117,5 @@ export const EmptyCart = styled.h3` margin-top: 25px; font-size: ${({ theme }) => theme.sizes.mLarge}; color: ${({ theme }) => theme.colors.textLightGray}; + text-align: center; `; diff --git a/src/frontend/components/CartDropdown/CartDropdown.tsx b/src/frontend/components/CartDropdown/CartDropdown.tsx index cd0703e19e..0e060e9f45 100644 --- a/src/frontend/components/CartDropdown/CartDropdown.tsx +++ b/src/frontend/components/CartDropdown/CartDropdown.tsx @@ -34,7 +34,7 @@ const CartDropdown = ({ productList, isOpen, onClose }: IProps) => { return isOpen ? ( -
+ Shopping Cart Close @@ -54,7 +54,7 @@ const CartDropdown = ({ productList, isOpen, onClose }: IProps) => { ) )} -
+ Go to Shopping Cart diff --git a/src/frontend/components/Layout/Layout.tsx b/src/frontend/components/Layout/Layout.tsx index f4e2da88a6..6a487e7621 100644 --- a/src/frontend/components/Layout/Layout.tsx +++ b/src/frontend/components/Layout/Layout.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import Header from '../Header'; +import Footer from '../Footer'; interface IProps { children: React.ReactNode; @@ -12,6 +13,7 @@ const Layout = ({ children }: IProps) => { <>
{children}
+