From 3ead415f03e17b45a756cf228396461a114b2ff6 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 15 Dec 2021 10:11:28 +0000 Subject: [PATCH 01/35] Add the global context object of the GA4 ecommerce demo --- .../ga4/EnhancedEcommerce/store-context.tsx | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/store-context.tsx diff --git a/src/components/ga4/EnhancedEcommerce/store-context.tsx b/src/components/ga4/EnhancedEcommerce/store-context.tsx new file mode 100644 index 00000000..e32cc207 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/store-context.tsx @@ -0,0 +1,166 @@ +import * as React from "react" + +const defaultValues = { + cart: [], + lastCart: [], + events: [], + isOpen: false, + onOpen: () => { + }, + onClose: () => { + }, + addEvent: () => { + }, + addVariantToCart: () => { + }, + removeCartItem: () => { + }, + updateCartItem: () => { + }, + getCartSubtotal: () => { + }, + checkoutState: { + email: '', + shippingAddress: { + firstName: '', + lastName: '', + addressLine1: '', + addressLine2: '', + city: '', + provinceState: '', + country: '', + zipCode: '' + }, + shippingMethod: '', + billingAddress: { + firstName: '', + lastName: '', + addressLine1: '', + addressLine2: '', + city: '', + provinceState: '', + country: '', + zipCode: '' + }, + coupon: '', + paymentMethod: '' + } +} + +export const StoreContext = React.createContext(defaultValues) + +export const StoreProvider = ({children}) => { + const [cart, setCart] = React.useState(defaultValues.cart) + const [lastCart, setLastCart] = React.useState(defaultValues.lastCart) + + const [checkoutState, setCheckoutState] = React.useState(defaultValues.checkoutState) + const [events, setEvents] = React.useState(defaultValues.events) + const [didJustAddToCart, setDidJustAddToCart] = React.useState(false) + + const addEvent = (name, description, snippet) => { + const key = events.length + const timestamp = new Intl.DateTimeFormat('default', { + + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false, + }).format(Date.now()) + + const newEvents = [{ + key, + timestamp, + name, + description, + snippet + }].concat(events); + setEvents(newEvents); + } + const addVariantToCart = (product, variantId, quantity) => { + const id = cart.length; + const cartItems = [ + { + id, + product, + variantId, + quantity: parseInt(quantity, 10), + }, + ] + const newCart = cart.concat(cartItems) + setCart(newCart) + setDidJustAddToCart(true) + setTimeout(() => setDidJustAddToCart(false), 3000) + } + + const removeLineItem = (id) => { + const newCart = cart.filter(item => item.id !== id); + setCart(newCart) + } + + const updateLineItem = (id, quantity) => { + const newCart = [ ...cart] + const item = newCart.find(item => item.id === id); + if( item ) + { + item.quantity = parseInt(quantity, 10); + } + setCart(newCart); + } + + const getCartSubtotal = () => { + return cart.reduce((sum,x) => sum + Number(x.product.price) * x.quantity, 0); + } + + const updateCheckoutState = (name, value) => + { + const newCheckoutState = { ...checkoutState }; + newCheckoutState[name] = value; + setCheckoutState(newCheckoutState); + } + + const updateShippingAddress = (name, value) => + { + const newCheckoutState = { ...checkoutState }; + newCheckoutState.shippingAddress[name] = value; + setCheckoutState(newCheckoutState); + } + + const updateBillingAddress = (name, value) => + { + const newCheckoutState = { ...checkoutState }; + newCheckoutState.billingAddress[name] = value; + setCheckoutState(newCheckoutState); + } + + const emptyCart = () => + { + const cartSnapshot = [ ...cart ]; + setCart([]); + setLastCart(cart) + return cartSnapshot; + } + + return ( + + {children} + + ) +} From 5b6a7b3f7a3730c54d308a6d3ae9e28332d3e81f Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 08:53:00 +0000 Subject: [PATCH 02/35] Wrap every page using a StoreProvider object used by eCommerce demo. --- gatsby-browser.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/gatsby-browser.js b/gatsby-browser.js index d2c5affd..eeabfff5 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,3 +1,5 @@ +import * as React from "react" +import {StoreProvider} from "./src/components/ga4/EnhancedEcommerce/store-context" import CustomLayout from "./gatsby/wrapRootElement.js" // TODO - look into making this work like gatsby-node & use typescript for the @@ -5,3 +7,8 @@ import CustomLayout from "./gatsby/wrapRootElement.js" export { onInitialClientRender } from "./gatsby/onInitialClientRender" export const wrapPageElement = CustomLayout + +// Wrap every page using a StoreProvider object used by eCommerce demo. +export const wrapRootElement = ({ element }) => ( + {element} +) \ No newline at end of file From a3a55cfd908f7fb6bf0663d5e5ffef5ccb9b8ea3 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 08:56:31 +0000 Subject: [PATCH 03/35] Add CSS variables used by eCommerce demo. --- gatsby-browser.js | 1 + src/styles/ecommerce/variables.css | 117 +++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 src/styles/ecommerce/variables.css diff --git a/gatsby-browser.js b/gatsby-browser.js index eeabfff5..19435776 100644 --- a/gatsby-browser.js +++ b/gatsby-browser.js @@ -1,6 +1,7 @@ import * as React from "react" import {StoreProvider} from "./src/components/ga4/EnhancedEcommerce/store-context" import CustomLayout from "./gatsby/wrapRootElement.js" +import "./src/styles/ecommerce/variables.css" // TODO - look into making this work like gatsby-node & use typescript for the // things that are imported/exported. diff --git a/src/styles/ecommerce/variables.css b/src/styles/ecommerce/variables.css new file mode 100644 index 00000000..8a251635 --- /dev/null +++ b/src/styles/ecommerce/variables.css @@ -0,0 +1,117 @@ +:root { + /* tokens */ + /* font-family */ + --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, + sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + + /* palette */ + --black-fade-5: rgba(0, 0, 0, 0.05); + --black-fade-40: rgba(0, 0, 0, 0.4); + --grey-90: #232129; + --grey-50: #78757a; + --green-80: #088413; + --green-50-rgb: 55, 182, 53; + --white: #ffffff; + + /* radii */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-rounded: 999px; + + /* spacing */ + --space-sm: 4px; + --space-md: 8px; + --space-lg: 16px; + --space-xl: 20px; + --space-2xl: 24px; + --space-3xl: 48px; + + /* line-height */ + --solid: 1; + --dense: 1.25; + --default: 1.5; + --loose: 2; + + /* letter-spacing */ + --tracked: 0.075em; + --tight: -0.015em; + + /* font-weight */ + --body: 400; + --medium: 500; + --semibold: 600; + --bold: 700; + + /* font-size */ + --text-xs: 12px; + --text-sm: 14px; + --text-md: 16px; + --text-lg: 18px; + --text-xl: 20px; + --text-2xl: 24px; + --text-3xl: 32px; + + /* role-based tokens */ + + /* colors */ + --primary: var(--green-80); + --background: var(--white); + --border: var(--black-fade-5); + + /* transitions */ + --transition: box-shadow 0.125s ease-in; + + /* shadows */ + --shadow: 0 4px 12px rgba(var(--green-50-rgb), 0.5); + + /* text */ + /* color */ + --text-color: var(--grey-90); + --text-color-secondary: var(--grey-50); + --text-color-inverted: var(--white); + /* size */ + --text-display: var(--text-2xl); + --text-prose: var(--text-md); + + /* input */ + --input-background: var(--black-fade-5); + --input-background-hover: var(--black-fade-5); + --input-border: var(--black-fade-5); + --input-text: var(--text-color); + --input-text-disabled: var(--black-fade-40); + --input-ui: var(--text-color-secondary); + --input-ui-active: var(--text-color); + + /* size */ + --size-input: var(--space-3xl); + --size-gap: 12px; + --size-gutter-raw: var(--space-2xl); + --size-gutter: calc(var(--size-gutter-raw) - 12px); + + /* product */ + --product-grid: 1fr; +} + +/* role-based token adjustments per breakpoint */ +@media (min-width: 640px) { + :root { + --product-grid: 1fr 1fr; + } +} + +@media (min-width: 1024px) { + :root { + --text-display: var(--text-3xl); + --text-prose: var(--text-lg); + --product-grid: repeat(3, 1fr); + --size-gutter-raw: var(--space-3xl); + --size-gap: var(--space-2xl); + } +} + +@media (min-width: 1280px) { + :root { + --product-grid: repeat(4, 1fr); + } +} + From 73d77d2ada47d330bc2caec5798e3c40f9b7331d Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 08:58:50 +0000 Subject: [PATCH 04/35] Add dependencies for eCommerce demo app. --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index 1db48d55..0ae426cc 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "gatsby-plugin-typescript": "^3.4.0", "gatsby-plugin-use-query-params": "^1.0.1", "gatsby-source-filesystem": "^3.4.0", + "gatsby-transformer-json": "^3.12.0", "gatsby-transformer-sharp": "^3.4.0", "immutable": "^4.0.0-rc.12", "js-base64": "^3.6.1", @@ -38,9 +39,11 @@ "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", "react-helmet": "^6.1.0", + "react-icons": "^4.2.0", "react-json-view": "^1.21.3", "react-loader-spinner": "^4.0.0", "react-redux": "^7.2.4", + "react-router-dom": "^6.0.2", "react-syntax-highlighter": "^15.4.3", "redux": "^4.0.5", "use-debounce": "^6.0.0", From 10270cb98899a5f02bd1e9d12c03eec3b2426854 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 09:08:11 +0000 Subject: [PATCH 05/35] Add "Go to Cart" button component. --- .../EnhancedEcommerce/cart-button.module.css | 37 +++++++++++++++++++ .../ga4/EnhancedEcommerce/cart-button.tsx | 20 ++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/cart-button.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/cart-button.tsx diff --git a/src/components/ga4/EnhancedEcommerce/cart-button.module.css b/src/components/ga4/EnhancedEcommerce/cart-button.module.css new file mode 100644 index 00000000..f0ac1730 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/cart-button.module.css @@ -0,0 +1,37 @@ +.cartButton { + color: var(--text-color-secondary); + grid-area: cartButton; + width: var(--size-input); + height: var(--size-input); + display: flex; + justify-content: center; + align-items: center; + position: relative; + align-self: center; +} + +.cartButton:hover { + color: var(--text-color); +} + +.badge { + display: flex; + align-items: center; + justify-content: center; + background-color: var(--primary); + box-shadow: 0 0 0 2px white; + color: var(--text-color-inverted); + font-size: var(--text-xs); + font-weight: var(--bold); + border-radius: var(--radius-rounded); + position: absolute; + bottom: 4px; + right: 4px; + height: 16px; + min-width: 16px; + padding: 0 var(--space-sm); +} + +.cartButton[aria-current="page"] { + color: var(--primary); +} \ No newline at end of file diff --git a/src/components/ga4/EnhancedEcommerce/cart-button.tsx b/src/components/ga4/EnhancedEcommerce/cart-button.tsx new file mode 100644 index 00000000..77d7b7dc --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/cart-button.tsx @@ -0,0 +1,20 @@ +import * as React from "react" +import {Link} from "gatsby" +import {badge, cartButton} from "./cart-button.module.css" +import {MdShoppingCart} from 'react-icons/md'; +import IconButton from "@material-ui/core/IconButton" + +export function CartButton({quantity}) { + return ( + + + + + {quantity > 0 &&
{quantity}
} + + ) +} \ No newline at end of file From cc368f4a00d1322d46083e154f987769e43d2010 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 09:08:32 +0000 Subject: [PATCH 06/35] Add "Navigation bar" component. --- .../EnhancedEcommerce/navigation.module.css | 36 +++++++++++++++++++ .../ga4/EnhancedEcommerce/navigation.tsx | 18 ++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/navigation.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/navigation.tsx diff --git a/src/components/ga4/EnhancedEcommerce/navigation.module.css b/src/components/ga4/EnhancedEcommerce/navigation.module.css new file mode 100644 index 00000000..f65a4985 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/navigation.module.css @@ -0,0 +1,36 @@ +.navStyle { + display: flex; + flex-direction: row; + align-items: center; + overflow-x: auto; + white-space: nowrap; + font-weight: var(--medium); +} + +.navLink { + cursor: pointer; + text-decoration: none; + height: var(--size-input); + display: flex; + color: var(--text-color-secondary); + align-items: center; + padding-left: var(--space-md); + padding-right: var(--space-md); +} + +.navLink:hover { + color: var(--text-color); +} + +.activeLink, +.navLink[aria-active="page"] { + color: var(--primary); + text-decoration: underline; + text-decoration-thickness: 2px; + text-underline-offset: 4px; +} + +.activeLink:hover, +.navLink[aria-active="page"]:hover { + color: var(--primary); +} diff --git a/src/components/ga4/EnhancedEcommerce/navigation.tsx b/src/components/ga4/EnhancedEcommerce/navigation.tsx new file mode 100644 index 00000000..4dd7e9c5 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/navigation.tsx @@ -0,0 +1,18 @@ +import {Link} from "gatsby" +import * as React from "react" +import {navStyle, navLink, activeLink} from "./navigation.module.css" + +export function Navigation({className}) { + return ( + + ) +} \ No newline at end of file From 3967d5fcc10ffed2f6433f9b2f2fed73c257b9e5 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 09:09:42 +0000 Subject: [PATCH 07/35] Add "header" component. --- .../ga4/EnhancedEcommerce/header.module.css | 28 +++++++++++++++++++ .../ga4/EnhancedEcommerce/header.tsx | 26 +++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/header.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/header.tsx diff --git a/src/components/ga4/EnhancedEcommerce/header.module.css b/src/components/ga4/EnhancedEcommerce/header.module.css new file mode 100644 index 00000000..19b0580f --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/header.module.css @@ -0,0 +1,28 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; +} + +.header { + display: grid; + width: 100%; + padding: var(--size-gap) var(--size-gutter); + grid-template-columns: 1fr; + grid-template-areas: "cartButton" "navHeader"; + align-items: start; + background-color: var(--background); +} + +@media (min-width: 640px) { + .header { + grid-template-columns: 1fr min-content; + grid-template-areas: "navHeader cartButton"; + } +} + + +.nav { + grid-area: navHeader; + align-self: stretch; +} \ No newline at end of file diff --git a/src/components/ga4/EnhancedEcommerce/header.tsx b/src/components/ga4/EnhancedEcommerce/header.tsx new file mode 100644 index 00000000..f3f93197 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/header.tsx @@ -0,0 +1,26 @@ +import * as React from "react" +import {StoreContext} from "./store-context" +import {CartButton} from "./cart-button" +import {Navigation} from "./navigation" + +import {container, header, nav,} from "./header.module.css" + +export function Header() { + const {cart} = React.useContext(StoreContext) + + const items = cart ? cart : [] + + const quantity = items.reduce((total, item) => { + return total + item.quantity + }, 0) + + return ( + +
+
+ + +
+
+ ) +} \ No newline at end of file From aab3e5328b56e292fc23aa6d8985e3606da80f39 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 09:10:09 +0000 Subject: [PATCH 08/35] Add "footer" component. --- .../ga4/EnhancedEcommerce/footer.module.css | 8 ++++++++ src/components/ga4/EnhancedEcommerce/footer.tsx | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/footer.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/footer.tsx diff --git a/src/components/ga4/EnhancedEcommerce/footer.module.css b/src/components/ga4/EnhancedEcommerce/footer.module.css new file mode 100644 index 00000000..e020d6c4 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/footer.module.css @@ -0,0 +1,8 @@ +.footerStyle { + margin-top: 100px; +} + +.gaConsole { + padding: var(--size-gutter-raw); + +} diff --git a/src/components/ga4/EnhancedEcommerce/footer.tsx b/src/components/ga4/EnhancedEcommerce/footer.tsx new file mode 100644 index 00000000..46da0435 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/footer.tsx @@ -0,0 +1,11 @@ +import * as React from "react" +import {footerStyle, gaConsole} from "./footer.module.css" +import {GaConsole} from "@/components/ga4/EnhancedEcommerce/ga-console"; + +export function Footer() { + return ( +
+ +
+ ) +} \ No newline at end of file From c0715cd1f5601bf53e345a238d0011f6687578c8 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 09:25:21 +0000 Subject: [PATCH 09/35] Add "Google Analytics Console" component which displays all ecommerce events generated by the demo app. --- .../EnhancedEcommerce/ga-console.module.css | 47 +++++++ .../ga4/EnhancedEcommerce/ga-console.tsx | 123 ++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/ga-console.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/ga-console.tsx diff --git a/src/components/ga4/EnhancedEcommerce/ga-console.module.css b/src/components/ga4/EnhancedEcommerce/ga-console.module.css new file mode 100644 index 00000000..913554a8 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/ga-console.module.css @@ -0,0 +1,47 @@ +.gaConsoleStyle { + align-self: stretch; + height: 100px; + align-items: center; + background: black; + color: white; + overflow: auto; + padding: var(--size-gap) var(--size-gutter); + position: fixed; + bottom: 0; + width: 80%; + opacity: 0.7; +} + +.emptyEvents { + text-align: center; + font-weight: var(--medium); +} + +.eventLine { + display: flex; + flex-direction: row; + white-space: nowrap; +} + +.eventTimestamp { + padding-right: var(--space-md); +} + +.eventName { + padding-right: var(--space-md); + font-weight: var(--semibold); +} + +.eventDescription { + padding-right: var(--space-md); +} + +.eventSnippet { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-style: italic; + color: var(--grey-50); + text-decoration: dotted underline; + cursor: pointer; +} \ No newline at end of file diff --git a/src/components/ga4/EnhancedEcommerce/ga-console.tsx b/src/components/ga4/EnhancedEcommerce/ga-console.tsx new file mode 100644 index 00000000..8b36ae40 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/ga-console.tsx @@ -0,0 +1,123 @@ +import * as React from "react" +import {StoreContext} from "./store-context" +import {emptyEvents, eventDescription, eventLine, eventName, eventSnippet, eventTimestamp, gaConsoleStyle} from "@/components/ga4/EnhancedEcommerce/ga-console.module.css"; +import Dialog from '@material-ui/core/Dialog'; +import Tabs from '@material-ui/core/Tabs'; +import Tab from '@material-ui/core/Tab'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Button from '@material-ui/core/Button'; +import TextField from '@material-ui/core/TextField'; +import {Link} from "gatsby"; +import {Box, Typography} from "@material-ui/core"; + +function TabPanel(props) { + const {children, value, index, ...other} = props; + + return ( + + ); +} + +export function GaConsole({className}) { + const {events} = React.useContext(StoreContext) + const [open, setOpen] = React.useState(false); + const [selectedEvent, setSelectedEvent] = React.useState({}); + const [value, setValue] = React.useState(0); + + const handleClickOpen = (eventKey) => () => { + setSelectedEvent(events[events.length - eventKey - 1]) + setOpen(true); + }; + const handleClose = () => { + setOpen(false); + }; + const handleChange = (event, newValue) => { + setValue(newValue); + }; + + return ( +
+ {events.length ? events.map((event) => ( +
+
{event.timestamp}
+
+ + {event.name} +
+
{event.description}
+
{event.snippet}
+
+ )) :
Start interacting with the store + to see Google Analytics eCommerce events here.
} + + + Google Analytics eCommerce + event details + + +

{selectedEvent?.timestamp}  + + {selectedEvent?.name} + {selectedEvent?.description}

+ + + + + + + Item One + + +
+
+ + + + +
+
+ ) +} \ No newline at end of file From d32d667562d95855c1e57098ce8659861612aede Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 09:31:48 +0000 Subject: [PATCH 10/35] Do not lint CSS --- .eslintignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.eslintignore b/.eslintignore index 3b059e04..e26df0bc 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,6 @@ src/images/* **/*.json +**/*.css **/*.lock lib/build/* node_modules/ From 6e778341e847529774cdaf23e3ad1e7d3316bc45 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Wed, 16 Feb 2022 09:45:31 +0000 Subject: [PATCH 11/35] Add support for CSS modules --- src/global.d.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/global.d.ts b/src/global.d.ts index d0a1de48..2e96de32 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -20,6 +20,8 @@ declare module "*.svg" { export default value } +declare module "*.module.css"; + declare interface AppState { user?: gapi.auth2.GoogleUser gapi?: typeof gapi From 33d0ffdece742509098d1bde6ee9dfc19497314a Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Thu, 17 Feb 2022 09:10:47 +0000 Subject: [PATCH 12/35] Address type check errors by introducing interfaces. --- .../ga4/EnhancedEcommerce/ga-console.tsx | 12 +- .../ga4/EnhancedEcommerce/store-context.tsx | 122 ++++++++++++++---- 2 files changed, 108 insertions(+), 26 deletions(-) diff --git a/src/components/ga4/EnhancedEcommerce/ga-console.tsx b/src/components/ga4/EnhancedEcommerce/ga-console.tsx index 8b36ae40..10d1d3ee 100644 --- a/src/components/ga4/EnhancedEcommerce/ga-console.tsx +++ b/src/components/ga4/EnhancedEcommerce/ga-console.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import {StoreContext} from "./store-context" +import {StoreContext, GAEvent} from "./store-context" import {emptyEvents, eventDescription, eventLine, eventName, eventSnippet, eventTimestamp, gaConsoleStyle} from "@/components/ga4/EnhancedEcommerce/ga-console.module.css"; import Dialog from '@material-ui/core/Dialog'; import Tabs from '@material-ui/core/Tabs'; @@ -35,7 +35,15 @@ function TabPanel(props) { export function GaConsole({className}) { const {events} = React.useContext(StoreContext) const [open, setOpen] = React.useState(false); - const [selectedEvent, setSelectedEvent] = React.useState({}); + + const [selectedEvent, setSelectedEvent] = React.useState({ + key: 0, + timestamp: '', + name: '', + description: '', + snippet: '' + }); + const [value, setValue] = React.useState(0); const handleClickOpen = (eventKey) => () => { diff --git a/src/components/ga4/EnhancedEcommerce/store-context.tsx b/src/components/ga4/EnhancedEcommerce/store-context.tsx index e32cc207..1b8bbaed 100644 --- a/src/components/ga4/EnhancedEcommerce/store-context.tsx +++ b/src/components/ga4/EnhancedEcommerce/store-context.tsx @@ -1,24 +1,78 @@ import * as React from "react" -const defaultValues = { +export interface GAEvent +{ + key: number + timestamp: string + name: string + description: string + snippet: string +} + +interface Product +{ + id: number + title: string + brand: string + category: string + price: number +} + +interface CartItem +{ + id: number + product: Product + variantId: string + quantity: number +} + +interface PostalAddress +{ + firstName: string + lastName: string + addressLine1: string + addressLine2: string + city: string + provinceState: string + zipPostalCode: string + country: string +} + +interface CheckoutState +{ + email: string + shippingAddress: PostalAddress + billingAddress: PostalAddress + paymentMethod: string + shippingMethod: string + coupon: string +} + +interface StoreContextValues { + events: GAEvent[] + cart: CartItem[] + lastCart: CartItem[] + isOpen: boolean + checkoutState: CheckoutState + onOpen(): void + onClose(): void + addEvent(name: string, description: string, snippet: string): void + addVariantToCart(product: Product, variantId: string, + quantity: number): void + removeLineItem(id: number): void + updateLineItem(id: number, quantity: number): void + getCartSubtotal(): number + updateCheckoutState(name: string, value: string|number): void + updateShippingAddress(name: string, value: string|number): void + updateBillingAddress(name: string, value: string|number): void + emptyCart(): CartItem[] +} + +const defaultValues: StoreContextValues = { cart: [], lastCart: [], events: [], isOpen: false, - onOpen: () => { - }, - onClose: () => { - }, - addEvent: () => { - }, - addVariantToCart: () => { - }, - removeCartItem: () => { - }, - updateCartItem: () => { - }, - getCartSubtotal: () => { - }, checkoutState: { email: '', shippingAddress: { @@ -29,7 +83,7 @@ const defaultValues = { city: '', provinceState: '', country: '', - zipCode: '' + zipPostalCode: '' }, shippingMethod: '', billingAddress: { @@ -40,13 +94,38 @@ const defaultValues = { city: '', provinceState: '', country: '', - zipCode: '' + zipPostalCode: '' }, coupon: '', paymentMethod: '' + }, + onOpen: () => { + }, + onClose: () => { + }, + addEvent: () => { + }, + addVariantToCart: () => { + }, + removeLineItem: () => { + }, + updateLineItem: () => { + }, + getCartSubtotal: () => { + return 0 + }, + updateCheckoutState: () => { + }, + updateShippingAddress: () => { + }, + updateBillingAddress: () => { + }, + emptyCart: () => { + return [] } } + export const StoreContext = React.createContext(defaultValues) export const StoreProvider = ({children}) => { @@ -55,12 +134,10 @@ export const StoreProvider = ({children}) => { const [checkoutState, setCheckoutState] = React.useState(defaultValues.checkoutState) const [events, setEvents] = React.useState(defaultValues.events) - const [didJustAddToCart, setDidJustAddToCart] = React.useState(false) const addEvent = (name, description, snippet) => { const key = events.length const timestamp = new Intl.DateTimeFormat('default', { - hour: 'numeric', minute: 'numeric', second: 'numeric', @@ -88,8 +165,6 @@ export const StoreProvider = ({children}) => { ] const newCart = cart.concat(cartItems) setCart(newCart) - setDidJustAddToCart(true) - setTimeout(() => setDidJustAddToCart(false), 3000) } const removeLineItem = (id) => { @@ -149,13 +224,12 @@ export const StoreProvider = ({children}) => { removeLineItem, updateLineItem, getCartSubtotal, - didJustAddToCart, - cart, - checkoutState, updateCheckoutState, updateShippingAddress, updateBillingAddress, emptyCart, + cart, + checkoutState, lastCart, events }} From 6c8515657860b0413e3df15be79ccd498c019d03 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Thu, 17 Feb 2022 09:10:47 +0000 Subject: [PATCH 13/35] Address type check errors by introducing interfaces. --- .../ga4/EnhancedEcommerce/ga-console.tsx | 10 +- .../ga4/EnhancedEcommerce/store-context.tsx | 122 ++++++++++++++---- 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/src/components/ga4/EnhancedEcommerce/ga-console.tsx b/src/components/ga4/EnhancedEcommerce/ga-console.tsx index 8b36ae40..c3cc111f 100644 --- a/src/components/ga4/EnhancedEcommerce/ga-console.tsx +++ b/src/components/ga4/EnhancedEcommerce/ga-console.tsx @@ -35,7 +35,15 @@ function TabPanel(props) { export function GaConsole({className}) { const {events} = React.useContext(StoreContext) const [open, setOpen] = React.useState(false); - const [selectedEvent, setSelectedEvent] = React.useState({}); + + const [selectedEvent, setSelectedEvent] = React.useState({ + key: 0, + timestamp: '', + name: '', + description: '', + snippet: '' + }); + const [value, setValue] = React.useState(0); const handleClickOpen = (eventKey) => () => { diff --git a/src/components/ga4/EnhancedEcommerce/store-context.tsx b/src/components/ga4/EnhancedEcommerce/store-context.tsx index e32cc207..cec54127 100644 --- a/src/components/ga4/EnhancedEcommerce/store-context.tsx +++ b/src/components/ga4/EnhancedEcommerce/store-context.tsx @@ -1,24 +1,78 @@ import * as React from "react" -const defaultValues = { +interface GAEvent +{ + key: number + timestamp: string + name: string + description: string + snippet: string +} + +interface Product +{ + id: number + title: string + brand: string + category: string + price: number +} + +interface CartItem +{ + id: number + product: Product + variantId: string + quantity: number +} + +interface PostalAddress +{ + firstName: string + lastName: string + addressLine1: string + addressLine2: string + city: string + provinceState: string + zipPostalCode: string + country: string +} + +interface CheckoutState +{ + email: string + shippingAddress: PostalAddress + billingAddress: PostalAddress + paymentMethod: string + shippingMethod: string + coupon: string +} + +interface StoreContextValues { + events: GAEvent[] + cart: CartItem[] + lastCart: CartItem[] + isOpen: boolean + checkoutState: CheckoutState + onOpen(): void + onClose(): void + addEvent(name: string, description: string, snippet: string): void + addVariantToCart(product: Product, variantId: string, + quantity: number): void + removeLineItem(id: number): void + updateLineItem(id: number, quantity: number): void + getCartSubtotal(): number + updateCheckoutState(name: string, value: string|number): void + updateShippingAddress(name: string, value: string|number): void + updateBillingAddress(name: string, value: string|number): void + emptyCart(): CartItem[] +} + +const defaultValues: StoreContextValues = { cart: [], lastCart: [], events: [], isOpen: false, - onOpen: () => { - }, - onClose: () => { - }, - addEvent: () => { - }, - addVariantToCart: () => { - }, - removeCartItem: () => { - }, - updateCartItem: () => { - }, - getCartSubtotal: () => { - }, checkoutState: { email: '', shippingAddress: { @@ -29,7 +83,7 @@ const defaultValues = { city: '', provinceState: '', country: '', - zipCode: '' + zipPostalCode: '' }, shippingMethod: '', billingAddress: { @@ -40,13 +94,38 @@ const defaultValues = { city: '', provinceState: '', country: '', - zipCode: '' + zipPostalCode: '' }, coupon: '', paymentMethod: '' + }, + onOpen: () => { + }, + onClose: () => { + }, + addEvent: () => { + }, + addVariantToCart: () => { + }, + removeLineItem: () => { + }, + updateLineItem: () => { + }, + getCartSubtotal: () => { + return 0 + }, + updateCheckoutState: () => { + }, + updateShippingAddress: () => { + }, + updateBillingAddress: () => { + }, + emptyCart: () => { + return [] } } + export const StoreContext = React.createContext(defaultValues) export const StoreProvider = ({children}) => { @@ -55,12 +134,10 @@ export const StoreProvider = ({children}) => { const [checkoutState, setCheckoutState] = React.useState(defaultValues.checkoutState) const [events, setEvents] = React.useState(defaultValues.events) - const [didJustAddToCart, setDidJustAddToCart] = React.useState(false) const addEvent = (name, description, snippet) => { const key = events.length const timestamp = new Intl.DateTimeFormat('default', { - hour: 'numeric', minute: 'numeric', second: 'numeric', @@ -88,8 +165,6 @@ export const StoreProvider = ({children}) => { ] const newCart = cart.concat(cartItems) setCart(newCart) - setDidJustAddToCart(true) - setTimeout(() => setDidJustAddToCart(false), 3000) } const removeLineItem = (id) => { @@ -149,13 +224,12 @@ export const StoreProvider = ({children}) => { removeLineItem, updateLineItem, getCartSubtotal, - didJustAddToCart, - cart, - checkoutState, updateCheckoutState, updateShippingAddress, updateBillingAddress, emptyCart, + cart, + checkoutState, lastCart, events }} From f60574318e543d09547be8c19fcdcac22776df98 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Thu, 17 Feb 2022 09:22:36 +0000 Subject: [PATCH 14/35] add EOF --- src/components/ga4/EnhancedEcommerce/cart-button.module.css | 2 +- src/components/ga4/EnhancedEcommerce/cart-button.tsx | 2 +- src/components/ga4/EnhancedEcommerce/footer.tsx | 2 +- src/components/ga4/EnhancedEcommerce/ga-console.module.css | 2 +- src/components/ga4/EnhancedEcommerce/ga-console.tsx | 4 ++-- src/components/ga4/EnhancedEcommerce/header.tsx | 2 +- src/components/ga4/EnhancedEcommerce/navigation.tsx | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ga4/EnhancedEcommerce/cart-button.module.css b/src/components/ga4/EnhancedEcommerce/cart-button.module.css index f0ac1730..7715893f 100644 --- a/src/components/ga4/EnhancedEcommerce/cart-button.module.css +++ b/src/components/ga4/EnhancedEcommerce/cart-button.module.css @@ -34,4 +34,4 @@ .cartButton[aria-current="page"] { color: var(--primary); -} \ No newline at end of file +} diff --git a/src/components/ga4/EnhancedEcommerce/cart-button.tsx b/src/components/ga4/EnhancedEcommerce/cart-button.tsx index 77d7b7dc..f7292a0f 100644 --- a/src/components/ga4/EnhancedEcommerce/cart-button.tsx +++ b/src/components/ga4/EnhancedEcommerce/cart-button.tsx @@ -17,4 +17,4 @@ export function CartButton({quantity}) { {quantity > 0 &&
{quantity}
} ) -} \ No newline at end of file +} diff --git a/src/components/ga4/EnhancedEcommerce/footer.tsx b/src/components/ga4/EnhancedEcommerce/footer.tsx index 46da0435..0ad126d8 100644 --- a/src/components/ga4/EnhancedEcommerce/footer.tsx +++ b/src/components/ga4/EnhancedEcommerce/footer.tsx @@ -8,4 +8,4 @@ export function Footer() { ) -} \ No newline at end of file +} diff --git a/src/components/ga4/EnhancedEcommerce/ga-console.module.css b/src/components/ga4/EnhancedEcommerce/ga-console.module.css index 913554a8..7569c511 100644 --- a/src/components/ga4/EnhancedEcommerce/ga-console.module.css +++ b/src/components/ga4/EnhancedEcommerce/ga-console.module.css @@ -44,4 +44,4 @@ color: var(--grey-50); text-decoration: dotted underline; cursor: pointer; -} \ No newline at end of file +} diff --git a/src/components/ga4/EnhancedEcommerce/ga-console.tsx b/src/components/ga4/EnhancedEcommerce/ga-console.tsx index 10d1d3ee..d6c8a0e0 100644 --- a/src/components/ga4/EnhancedEcommerce/ga-console.tsx +++ b/src/components/ga4/EnhancedEcommerce/ga-console.tsx @@ -1,5 +1,5 @@ import * as React from "react" -import {StoreContext, GAEvent} from "./store-context" +import {StoreContext} from "./store-context" import {emptyEvents, eventDescription, eventLine, eventName, eventSnippet, eventTimestamp, gaConsoleStyle} from "@/components/ga4/EnhancedEcommerce/ga-console.module.css"; import Dialog from '@material-ui/core/Dialog'; import Tabs from '@material-ui/core/Tabs'; @@ -128,4 +128,4 @@ export function GaConsole({className}) { ) -} \ No newline at end of file +} diff --git a/src/components/ga4/EnhancedEcommerce/header.tsx b/src/components/ga4/EnhancedEcommerce/header.tsx index f3f93197..84d75538 100644 --- a/src/components/ga4/EnhancedEcommerce/header.tsx +++ b/src/components/ga4/EnhancedEcommerce/header.tsx @@ -23,4 +23,4 @@ export function Header() { ) -} \ No newline at end of file +} diff --git a/src/components/ga4/EnhancedEcommerce/navigation.tsx b/src/components/ga4/EnhancedEcommerce/navigation.tsx index 4dd7e9c5..66bc6e2a 100644 --- a/src/components/ga4/EnhancedEcommerce/navigation.tsx +++ b/src/components/ga4/EnhancedEcommerce/navigation.tsx @@ -15,4 +15,4 @@ export function Navigation({className}) { ) -} \ No newline at end of file +} From fc38f1d96b8f1ede4a485052086c736c7bcdd933 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Fri, 18 Feb 2022 13:07:06 +0000 Subject: [PATCH 15/35] Add the product data and images. --- data/images/azureT.png | 102 ++++++++++++++++++++++++++++++++++++++++ data/images/blackT.png | 102 ++++++++++++++++++++++++++++++++++++++++ data/images/blackT2.png | Bin 0 -> 3972 bytes data/images/brownT.png | Bin 0 -> 12700 bytes data/images/greyT.png | Bin 0 -> 11764 bytes data/images/pinkT.png | Bin 0 -> 18638 bytes data/images/yellowT.png | Bin 0 -> 23884 bytes data/products.json | 68 +++++++++++++++++++++++++++ gatsby-config.js | 8 ++++ 9 files changed, 280 insertions(+) create mode 100644 data/images/azureT.png create mode 100644 data/images/blackT.png create mode 100644 data/images/blackT2.png create mode 100644 data/images/brownT.png create mode 100644 data/images/greyT.png create mode 100644 data/images/pinkT.png create mode 100644 data/images/yellowT.png create mode 100644 data/products.json diff --git a/data/images/azureT.png b/data/images/azureT.png new file mode 100644 index 00000000..2496a0c5 --- /dev/null +++ b/data/images/azureT.png @@ -0,0 +1,102 @@ + +ecommercedemo.googleplex.com - MOMA Single Sign On + + + + + +
moma - inside google +
 
+ + + +
Single Sign On
+
+ +
+ + + + + + + + + + + +
+ + + + + +
Debug
+ +phgg2.prod.google.com
+ \ No newline at end of file diff --git a/data/images/blackT.png b/data/images/blackT.png new file mode 100644 index 00000000..bc2e4e37 --- /dev/null +++ b/data/images/blackT.png @@ -0,0 +1,102 @@ + +ecommercedemo.googleplex.com - MOMA Single Sign On + + + + + +
moma - inside google +
 
+ + + +
Single Sign On
+
+ +
+ + + + + + + + + + + +
+ + + + + +
Debug
+ +phgg2.prod.google.com
+ \ No newline at end of file diff --git a/data/images/blackT2.png b/data/images/blackT2.png new file mode 100644 index 0000000000000000000000000000000000000000..679064039a9ae3ffded252e2a493e9dc19e70fde GIT binary patch literal 3972 zcmV-~4}0)ZNk&F|4*&pHMM6+kP&il$0000G0000I0RSrj06|PpNLmU201cPMplu{a zpYR7gzvU}|h{(f18A?h@N*>4u(uNqyK@4RPiQyj^vLv3A4g1@+Jx|+QpH@{ws1#R} zLMfOigi=B#F{ZCY}RaVTL4r3*9AG?pFxBBpV(^v)9;}3Av{otc#-#JGn-Sq|CL+HSE-!uH& zw*D#Oxgg@Vyv!oy4>ffh>P=v_^Y(4^Ts5}7@a`wbzUmcHaBbdF za^iS@9Mf#&H7hH)j{B|R2gZclieK+P8)j%NsY#bN-)*t&PkGV*)g^9g`#)&#*otgz&)r@^wg?dABGUn z)wZJ$b@Cp)f8gVu1fm&rI~q{T-thAU1ivM(I5Fw1J);47DZr6{m{fp9%PwJ(7mXH7V3Hq=Iy^zlS&B{zCNO151v)L5zzo-Mw3ylR=RwD1mq_mhbev<%kyb8R&YvW$0<;__y<)T+C#@VOT8@!c z6?&d0t>x%BOj@<**%>7bPa%5t1xce3O;?;FjXTlw@~=tVUWcX|x=6haUFYYXBjska zz3B)kH=*tI$4Ipjedp(%BgJMkzTqe-wxIF#$4ITvj>ZdeE|5|aTHkP#lv>gH`eUS$ zSBTaNT|=bMi00QFCxvD-zy27D+iTH$v2B<|Yta6Nqb#}_?YEs^(N$=_G&;c|J`ehD z`IJTKu>h?nS!6X9pn80k>l*b~0Ap1r*R8-3w4UU;4OoJP{wUX5i6wB2rP-z%mp#hq<;E>(JQ2wac&$Yfo`) z4c4K0bdIZjEQHZ;nyckliM6M=dNWpHRab&50jz|lcz`R#Sc(2YE)l{OAqy%dbeSLrf+prR` zPu}J7oBzDK+=fM%_~;Injx@2vArGYM|djHac35mf|-Q#sD z87N-HuF(;BF6LX<|^iGlOg~<6<7pmNb z1*yS$1h62jSdjHtk3u`vqX`SrjP>x8U_Gj_Ah#mw0%}wtY7{lfu^x6O)*}}Sl7|Ix zA!{5Z5(c71Q6lW-|GhX$EZ7h=h7zG%L=B@vD947RG1Lg<9A zgQOQxWF3+YqeugiPN7CAlE%WQQG=w@D6$ku<0w*$r2gpyO5`J{tGWv%T!{MQyC~s6 z)Q0I1R9LVf>SOPt!khW*}(3DYrxr0kLzzm%oV|cDTM%a-!|E|e)l%mLJ? zN6dEA`7KgLI#9=tlm}5L2PsdYP~47?gD4brB4pky>Wmd3WMc>FoNPtNZ3j@Nzr=%# zf$#+C3|1m!T@UI6eF%Bmx2O~JA!OMl)cMb^5VCkIfiht?LOQ(@C=+uaWyuK2L|jOj zKZ!CC7gFB%ZkFnTQK1-C>l8xRBCL990rY z8&bN%C=+!er8|r=Q72M5qbL)%V?AOHtVi6A^@uvL9&tNT+T$n_bs}X9b>eoU45Loi zjg(>33A>Rpj5_l<2pK}5|Naso$5ANkM#xbVn$1JVA5mz^kB~hmH04LeK2)0V*pV(g zn*4FVK&jDU#EOrPC4U+pjiKJT3PhUe`EFnoWq+DI>9ph9TxPDGiQs~{}{1-f_kBh3r1p>%NX zP6X=O`9=Y%COUTxA=9zv?70q-v1lwwG8*HG$LaR^eqB(Mbdb%HyLKbkXeh=abJ6fj z@Lv;S59~^Xvih?1+FFxj3ntycYJXcolJLs)#a;MR_h}*8&?g|2@ZVQ8$v^u`oHP3 zSKb^*dNFr)B>CMy42?|=UK~m;6c_!jFq!LdkXB+g8JrFNeRKqc0RRA2P&gp`1ONbV zQUIL+DiZ-L0Y1@Us!ymTq$4JkdRYJ(32AQKU}-2mW-pnE^H%UV{mOB8y)Qc+HY(1L zO4W*#0rp;ha{6xCAWtDSeuLL2yO?cmt&s@G=oT&#x%q%M*?;oGx1mrW+1P3XlMgXw zU<>08+NfK<12c!CxK|*yTOwG+w*fJ3(!sSxdg~|am`c+WGO9EJJq)MG)pkJ2*(CKD zjssU7@3mUTZ59M#fP7cb0`@p*sxJ^zA!iF!h$mWG8cceq6cwt@iJnkh%5UE6%zf-B zknduuX7#Jlvs*1?_s6h@B1A^>;FXAqtpSSf-SLchUHh%JyZN-j8H{$mdGITP%$<%0 z>3hF`-^bs*eKPEb;nsg25QcR-pxrlAz4mh8MSg%~I?PIRULd58FsRhz+YFit`jehi z&ny&)Ms~F>kEq zCz8=uU0iFW7A~^+ep})XBwB>;bY*1{G6cWwhV$)!v z;2tJpJ|cU$d_c`dBhEgs35peSMCD}sxe@j;ZJ@|{V}J@_{Zb24q=Lwi#Gbvqx$<*Q zYw#63;dDDvS>Ysus&%8_`}}#c>*N~kA@t=U1oJwIbuI^P_eK9vrD0Pwk}`SREWTh3 zt-5qvURfp_0YdCAR41vjKLW}M+xpvu1{`uu_k1G5AENeZC$xtwlY1k|_3YMzm;HoN zEX^xXCxB*5CGz`qwYAB}J2}1t>X4O?zkQQ|hLCaYp#HJ?Xm%{dK`MX%{`7&xvG+f# zfb+8}%TOQQZ!52ZQ)9;`Mfg=_^ z7KmFMhZyUjG>wGw%)M|ebtD7b4H)eUb%I=O_iw?hgA?(w4`ADgn*RUBDQEptjDwI7 z2BY}G<4jXHZwFee`Vs*y$?H6SIT-Bi%}u4W$mdl2R~T1f)h69W`!2_;9qIji+CFYi zv5pNB^(dJjP%)i&v3T5zH}-Znv0lME>IK+ICYHd4ieb%NN4DLvIGDFtw~Z@-&w#st z=h-{b^4J?&G$W<3_}LcZ>aa^u@1#w4TY6N}?+Sxj84v(2o69KpIbZC^Tgm2~8; zkTTPQncn)LSqUK#B$Q6W7N+|Qznd`Onb;Q(g9xPd1bhIed$Dy+Q!XwOp>c_zFIkw8 z$iW19hQ~zctvuDkgn_C(;A=NZE9aqiW36_ITG8FV=iiy`>}>iG!e}nXAfkz^1a~tU zprvsdjWpbnB*(Q8iGaTvfRZx}RpxUzb@(~C@<327qE0vIQ#M5~eL&Wo`Aer{qjpBv zKz$L!lOs42KsUZ+`~LvL^Z8~@t)AJyG2oGyA_7LD*=5Fm2bWlIeMT`CE9b!m1C#)b z1kfq&^W6f5PlVxRH4smPYaeyJscXErd|$hPp=Jh%UmvfHGu)`J0Hn__`a1RnZxCD161oz+$kL>=t`@VDL>#3^l zKGjwCR?l>6$jiv2vV(!?NK2?`tMTa~fPsObeS9_`z(^s%#Fflut3&0;(6@7Jj^S3iv1|Me`eaU&^OM>OcQOTbN#NhYC5 z49d1rN`V_0WI*8$Iebgll+S@!3~@FM0lRgm-n~V@Z6?U^ zH9)+3>i_*ZDpARb&QAAeTYEdJV3HhZX!zbaGCeX74Iqd~9;$V2`#UzBw59t=fk@u^ zfgVwPW_5XUpOUl9^Yl048SHWXp*T_u>Nm9(;+mNjPIQj=hK7ZHK&)}zDy|q*E^L+F zNX+8UU9qG&n&j<1^7zHJ6-vJNyRzsTI3i%JVc6~Xh6=y;^SAcs#}1>$=BF9o{`62L zh08m@T-mCX+;v}sR@%DB&UQv%me>v{T6ObY0x5B(0WG4|--$m{&d{x6gifduARvT8L9O&S8q$Gqg&0>7a_zL8!aMW( zindbT!c+dLgIc;F`Z8Kn>A4J&DP%6qt>bO%%&mi{M0;U7%SvX(aHq6OX0}&PP1h+y za0!qF4<*@aemK=@PKOtp6c_m&dzUjbzGNb5G-SorRkz2ac?BR})h~^}E4=0D1e53P zABbvWIqAF%xGOo!xht7)XiVZ-{cb^P7@sJ-65xenSOch`IWY))D4r}(DJrUsyf1{= zmn}H;H$j@HwyTjKjuX6l4mg)4HB4**;YFmE{bp?$o)MQcw8Li5Xsc^HE7+gz%9Zsf zW;!Po_Iv$G&NaFGy^827yQ4RS-_2(%VaBbM5D2Y{iohxW-r3k%BcfL0g*7s%Fs68M zHS-#xdgYjKJTXA>5Lo1J;4=TSrH0%2Wu0mDQ>QCorFM&5=>2jUA3@4Dn=NmYD4zBzPHuiqy`(vZ|Py%1{ zTH6qyn*mbHL*B-v0V%KQ1Wsg5d7)DQf+WHB8|kY z*wpvuNW~r6*g>_yLQp>5aSQKVemjQb29wrLf~#r#>+CbF43%3NH8#cD##Icik+ARPU;?)(wfE_(gdJY>gxn$6<7uh2bi=zHN2Ovk#YOIeqBKPhz;)htENS@k)AgVT z0s@c4?uC8IwY8Sh#^S0EHUnE+(Hp%6y9I7q8k>u{CS*N$xXw6>dj4hNRBx=koGF{$ z8(Z9SB+8=ba1e-65hS&{?Os>BtA6Eg2Q1ewz0Drc>CN1N2#N27)B|st`5U(=pJ&_; zHf04+RDm1owA0={%~WsR-7=Z9-(9)3zFWM{gR|<5d5wgEagpQ(Ov>^!8j8U!3X6Jt z39C+V#o@Q+|K;nEe5&CEAeM6^Tq!z1f= zm{G%lo8Mh?|* zNhIM?E&2|Q?||6&ZD?LahLc3h%P4?HipCe}$oYa_4uDx`#Pp%ItdP!3N1XtwXQR{Xw0m%j36vc5BAO?23{-fqNx|62p z%F=&E$tprB(%;0TrktnLEFClA=u;@M9rQJah=r}Fyr96;q|naK(&PB?I-+IBi(d%S zrhiFtb8A=hfjI-6Ii{sB>(IStWH~!dq$wDYGGB$&bb0I82~f5rlebJ zedSzv8{M~HOT1AUwKfXKx~`L$4Z0I=jAEP?Zju*UY{TqUkY{}`XSL=O^)JNRb4S%@ zy^yayq$dJYa^s&4N!^KLU%0zU0Oy!+y0y;9Zo&ysTKB-xcX<5g$V9oR-+3dtgJ3o` z0>MdX2{Q7-1r!#gzsYCoIifSQkdgu-bb#N!k|qZvQd+0+$_t(ZKrxF7$+o}ZsaJK% zXX_cDH-A_y6DjPlAxlQvmRoKsT|~5^6X}Rk z*gZBiqXiFM$jc*4^EFYfuma#2f^|z4g3aAS zCVdU;4V8T@v1H+}4!Gmjn2o>^IVC2?b_GiGUr!~+I1D&#Ok>d>N+BWpuF8oEx;!k` znU>@e^Sfc+C;4nip)K_&AM>LPaOZs}#mB$JCA*+dHLNR+@h1)VijePpmNPvXKDakf%2AD?WD7I_@IhCOs29!)b6ca#V8tTiG~ z!(FbJT0)^lg5bJTj#`I&R+mkjftw=1V~&ZwiNw1akIa3Xdt_%8RX$Gus8iBLvGvg31-)Nj_czlcViYsA48et)+=pE*chH z!81U#;@6}!18+Vpb~f72^gAf}={Vi?Ylp^%JB?kJYdxGm;*gcu{4X;(=t>`*V*wR1 z9If4>rc0FBgaQ~$SD{gslgT7FOUps_4kAB)FH+$z#M#sKFNVpGJw0r7JquOoM=~Js-p@ex$MJTj>!ubB0 zAJaHO*k6f#o}%ew%Tk;nX|=J99tVWd066|au1(SMp#-m{jsnE9j^r(owGKt7oclqC zbi4u1J4t#0hY8MQEVLnZHJy`rixlk>xNUP5QRnTGP@cN6hwWFe-#IfdR{-53c?)En z%Eap{uW*h)lT^Vr!=uM!09iIzDs_PyXVgzoxXJj7uk2pEIg*<9e z4A==|=_KHj8~JB>Yf%g~hm!AKC(1Qcw8TE9QV!W|nTS|KIZNt%|7)OxR z1?H@6g_6}B?cQ0yxyW0`tfiBW+6lB7qJTQz01i44bW2E5ZOZr{``Y7uo-OJ0rS5(} zOw!&E<9aEY-sHE0KWPpnZQBgSdQn_;{*iT`L%4LFCHxk)$R~zYPyDpde|H`mc2}Y~ zJ9PHxw4qsfPLdh(tO5^)82whF#p*hTG7RC|7OaC<+t950uUNWwB|thxJBln+?cK4g z{)o{YYqAd@VqopX?^q8hpFQqwYuOUJXEVRv1i2hWpT`j5ETc;j<9fafOk0{M4zOyE1FR3h-E{^t***wVhAf$2()=S2}S+?a^L3asvy(WO5Vv z5_cC>Pw;2f4)NKD&l$e*t4VfKP7s7HfKlT9gfAH=CX)3Rk5Ggmh5}*Wy)49iAzwP; z=4hxo+(yF8Gf=XCr@kWMO+R7K&qBx=4I}I`DAu-QTFyr9F3`@F38U^<(a$b1Z zl3i6weB5wbORvz5m)0Jtdk}ym@94%}p4bq=o`sRD9^NiV`#%)R9$j93kehqo{lV|P zx?L$l-wn?2{l38o30no+KFrFbtaVLcu^znQba}C?+Wf4iB2(INsf`&(#7@N?fXUUXWsiypDt6V20|jCZ_oGDjqoU*jMvX`nnos)TJ~QDIPrA zY4h%rxdGqqJ4)p)otE|(+v4qh0$ZHkpS$`?qVLB9--Z@$wM&@ReTzL)Z?fB5+RAQ% z_x4~)>}-GaZ#vI%EZ(1-m@P0KDcHoCXpfNTzj`{XwLJR=q~&bIb? zlAi^l-l89yaLxf1WKKOdc;+Huk`TUbWCmKU?oN)n`~SRzCj5;-diORYs6d0LJoRAp zj`bk1Z1T+@xus;`O1Wy#meU}`GG{PMa{#7&>poc>Ggyr+8Hu_dC>lw*t;Urb7eV2i zGicw+mz&_&c75d<4=4UI_Q=vGNr4p&LmAxmqoA~m#!47_qWLiJS2IuJCwr$~p!Wmi zL;v3ytjQBY1&t(}MVH?UMm-#f0_Vb8P|qlyZ-t$kkd!a@#dvNUjh>C;4EM)RU*T`)@*%aT(3lu_-m-)tL9pn1QiG~dpl74Q^t zlnQk`r&NqIeq7R-*}OcU8QrXNY$$9MW=%+**b%2C5Ogcbv%iq~w9_4&=ri)he`w-K zH<=-E`Kw3y$*~9q`fwYvTmiiCRk(>QtTB~xRR$Gg7;L{pzQ%|Kh=rZ0m2J_XT#b;C z>L6NikZEZpTg4u^(>Cvbo~e~;;iX(-{@6qty9ibs#F~hqtCAW`9Afwv0M*Ad5b} z1oumuwFTb7!%~klBV>7mC~jU{qifm-4Wwe>F7F}p^b zj7l*H=_@3`qwIWf9YK7qj4;i(!_7=$$c@4P z%)oHW%piIy)h{tzQE7P$7`K_oWWJff2R;RjFqVWusz>W$RO$+GLQ!$~MNqtCdDK@N z0Fi_=!HkTkLL>g)uT0I>J7^5EVIprh*eeMlC`DwG98K&(p&?C$i6WZjoKJjVA$&12 z!>VdY+}68j2(xiA356u31b;o#1Oa#GMijVQkf1AMDc|vL>v4*JnB}t20{CLWl{2GY z#|>C{xzfT4Gox^0+gzZO7PQK3aYki!&0@S7w+Rq|XHl9(Xt@lD{$tlxA;F`yMgJ{d z%(e`ptB@gG1Z3B`D^@cb{`60G%znCX$O$eXSGcciML9VULrM1iZ@6e@#KHkoYu``k z5YQWo@)ElfB*N^itsD|?-Q}d1tq)c`)uL>8SM@d`B_3R7CJ`JV+%v}{1#3J+kN>c4 z$%rD-sKFM%zK$125l1Las~!*?vjNS-1>cX~3sK-5p;v8?i_yBct;?uGB>bF6S8d{o z(O&mx+~)9P$tfV44@>1Ohvr}lna67pDsS+6Ue2Tf{E;8Aw3`{> zpWg|$njWE_FJ^4T11Kqh$XL%M317=c2ya>3oFTBI^Sz-Ai6rF{E+lY8W#nZbwX8w7 z-2BbN!g29MG&+RSlB6*lKsqTzShcisfu=u8R4C=7Kiri;ZU?hI>BEet(S&*+%>k%a4BAqB}=#G*LedtI)GNx;V&)NU^YIaSC) zLP_8S;cg=hDN8{Y>kau=SP6m|`mmrD1?#BeX22rbOB(7=i}j$o>jlGfC$!^L&h!kh z&wqtN?u_Zv1O>~gWnO>+=R~kd@m_t2)97WMF^1LgQXtPxBrHB3ZmDYyh2KqVN?GJu zZ}F!NMco-nHBlO+4#k~6su4oA^9HYe(61BEmNO-Ee^0rH>l+JK-8inS1q9!;U`HA5 zSNw9ZYY$RM8BuDVjz;4!}^L#`?9U1^KbjYA^R6%SejH?H6RO%V^*sniti&@ACpE7vE+rw%O zjW@B5FSQm!Uq%+Tn@jzQ)%%$ zK~{7AygnehX)p5jy#l=F`3$MFp_u8fzXr^&P%&H8s!$9BqC|L*HO9;jg5Hm|(V!q<8sxNO6q;|h+3fexuy_P>OkIvCK27U$SE)e$Q zh=PEnRaF0D{aitg)mDbHcP_XXEwH5Td^$QaKhkdE!;Z?M2fQ1rnO`#Cc^r&I9Jd zFosswN@tSV4%$5c^yb~$UK~2S$SnUCw{4F=r4uJNVx?T0N?6BEpH|o;{_x-~6QOkZ>)#A%&;3lfi!~$db zK~fscWUg}IJEs4Iq32>_ARxBK>&X2uiO z(T5#kTv}js+qs?L;vS=2jk&|cU;-Y9-RD)mz)IS`EqI*psLc-^KKj0AiQA~7QKUdg zz~|PGH3Z|dd(ooWP&-eR2Im?dD;=xI?M0=wVJ}dtG~< z6ihbJL!lMSBmAxZH195bM_JQ=)DYYT(StRZc#Jo!C_Vu*Q&$<64ebPNWJL{D&>ea!1T_g47xsMu}W|sU=RpsS+`8%n64!+`B zfS=o}p{7^H&%B-b9Qo#l&m3$yFU6w#!r?v;A9r~l_j+XY*kEAwg5Y`J44V*SkRmhI zb?W6bRG?xmylpT7cq@l(7uKi=Q> z-UBCS;y=Rh%PfL77mm?qCx2@y8qhS+LTJyk45(i-Y3T<^maOx^L58!I;r8 zhRbUk23sIPx)0xxmAs(OIUcdaRsLVse*3Kq39}A_#VCLI-6aP3;Kz1Cm&40eN3xgY z%!)xfwy#)9PyUD&Yd<|FuOpPNYaxY*SkRPAT6FjqL(6GITT!iD#nb&tjmLYk0y*9+ zUVg|x*=;=-vc9*y5#945k4SH|wSSuS(hzs48-!mooirvSazEr~O8i>HFEq^f{(~oM z#$R03J5{baC%WDmr8uII>iU7T;tNkVrM66UydFx;L{z_q+xw#%;t*-2-aA7c7{VF8 z2?H_P6bZ47K!5(1k~3?UPll($azEdXXFF>gc$uFAEGzW+_R7zC6O!){)dniV3fop} zw^_Z6{&a1z`&&yuw&I<$uWg_I8j?OaiS51q#Yz#lCoC;Dtls*XVk6b_0`>L^$BTFN ztjkQ%9xlW7P2xWM{4shbz?WWK7^x3m6Srf( zvgm6c_@Tw~C?mIqT}1Lh0ei0g-9rZa)6?19|4(rWmf#v*R!hJ6&b!ujNR#XQZyTEo z-1Q3*i^=ByhlfUSABj7^&nWC-zZH)Y%|Hk{#Dmq_>LaoA0z&{|&?RQ6Kg|s8#{V19 z<{i#5H(m|i7f{HA(iP4eU~sV1#0ke3X_gB~l3KYuoH6KN$}>PXZ~S*@VVrQ5XyfEL z7zg8RjD0A0>c_k;FHexyN{B8>YeotZIY3xxF2ku5y@&y!3?6H`8Zo(;VvkV9#c$W>^ZIhlLfyg?WYHO&t3kYJqW zN50q1G41f`2<*sXF#1X)vgwW%jR;9X?BSOmg`vu{#7qCjq>n#Lm;H9D@}n~r1ahx( zL}U1JShhTr=+HQGJHXt=`|!V6>--C+QG!5b`AWN?$BrLD2*p;1@6dH9x5N$Nu}6Q5 z#w1|x#J4}v|4(IrMVJ$`ZOQXKBCA4~tf7#`%y%XZ;gO4BnncI#mB%@$g9&S*P5o=l~yMWcD zB4=?1HV>oFX!j8an$ulfT7;hi=JfE7&hT?ReklAqSY3aEL1^gDt)Bc`wl$HlwaJ9y zh-Pv^Oabk=loVK|nIVU`%^+KvYAA(U3Sjs_i z=UNKxzf0^-E}N@v6MaIw((k^t`I*m1t*`&iG@i2n*WJ5MV!?0P1^HH`e{BptG*pod zTIJ~2+!;}JBJL&G&Pgu@^;`eUbTEaQmu8WoHN)b;|F^JxFk4R-ZANTTR7tFJ!Q2-B z?8D=#KqUQ#^+R;2&4j0PvoJ*O$0G|Lkkz!UDjyLpr5Z5c7BUNAp{{F!Qss*~O!!N> z*srZVD~R_;pQ=bleqXch|J@?Y=fZbJgLf-_MaWJ_UPwJlJ(6xqh|d|5u!G%j!cK$_ zKIPsc0VOP8)cysQ@N>4)e6+i8!4sbuD2*oivPv;TXAmKk!{5+^FIwL1)>Ll8=8Ja} z&UxxT3_$+Pnb4{gqV=Wb1)9ZY$dG4=mpnE7#XwucNVHKmoxfGU49!suL8m$1IHdY# zExoQkEY-xnx5M0k>=lGHGC{*$Lv1GiR{Ao+!aE#ruqm?0k95*@%5)>RXlC=V;n0nQ zVmVJ7(7OF6&-cTk@)x4#^%1*7z6X5-hkM>Pu(V(6B}pYP*%*s_`wGS5wfp%*!utw) zmk!aiH#WchL!xLRqP?9_A$Z+2rTg(dHx5Iivla*#EvAmmTqT8vc=d`u?9C#=O|V`I z)``9My7?lBmuCM2((g$%^6mmjOmIgH1z%|O${x%xDUbIoqh#C@?*xuXEugz2gd_IX z0T)w911(l18+ziSP2!#h$oxArg=-k^${u)Mp+gh zXNdi{5-~^WR8C;Fr1JCpI9&P{8~nZm;f(SkN*Qxe9?r-F0fz<-&Ok^vVE6L_G`99X zTUQ*CKOn6Vr{j)x|HlCn&S)R|{}uTgw{u7J;9FZT z#IILI!qk%}8J&!R9eP%z@1CxKb1bj_sdWD?%qqRRYUT)%5hZ5Ld#WuLCVBi4$v8|w z|Kal|FtE2b5eYB|8?sMRfx*cYy4LCpma`#`R)qC0rphvt6>!64Dd@X1&MWLQx!Jsg zI?nd4gVwkuDakFQn2|4j=JA_rhhR=Tb+R!mLDxZL2W@yw{-qD*0_?YES4L`rBR`p& zJ?G&q|AZ8FUOc8j({HpmK|R)TQd^35u&KZm<~SPfoZr>f-<9M{F)h!TvzzqH4_jX; z0=oN-m?;r8*&*ZlQMd@r;Y+=(^0_hg+S~1UOpP#<&s*u$eEPz`2+u-#)Lq}+@I?9p zx42PYZ^JW>r8zU{{(#(r%5r0?P)b0j$}dh@c~bY7d&<3HSi!smX2zv8n4<(!25%!< zDD~PjRWQdEpf7djn^CGZSC{r=ql3+@^h8c`zy*?MIWdn zGmKb@<1?cCOmpCO1XH;minS(-WJH$?WRa|WK$PV(==9(vLGc@x>{d;1gIJ5qJ=}55 z%`P83`MD!7(OB7jyZo*9)XSmv{#G&lMB+N6a%7*z!}2lR&JJy}aGS{l>&)h0QOQps z4^++Yi}jPmkE6ZSg%D;Tp*hG^M6yStGBNPCCy5)1q1zWyqixoTY%qv@vJ8>ILR2=) z?JcTQ2t+5llGHS>du&XDz7VMvrgw1kXf3x%zxGYeHM~?iaX1)KVLFz=r_UyAt_R#r zrgOr{CS+m+hjZE1x)fZ~V6gEckdy4=UB8u*s0-@XKovupr@( z>^N9TP;j@IZG5lGxFGN7+Iy(Z*+dQ0xFRpet+Rc8MdlATaug3j?Kdbevc+0Ws5mWS zQJ?9E`pe(s+ImaZ2$tfRFnk6CaZ-}R1e4A0qxZk$EYqY@?B$2&PP}jd+02|3Msm zfe~~X2FO*7J%21%GLcCicmC{U-M{u=WwJ*@1@o;xyRiV-i=~^oEs-AV>P_{ zpeNB)Sp9&K9LIUg>WAx7v6%+(5tTX%w#8x}%(L{)Grk;mn9a~RBBk3tBVC>e8M>-2 zOL9WUs`PVDkbHb(5aU}Eqa!s0qFc=_l>up~jFR3v4E&T4LPkH9Ni$fCBh8eACit=o zgnMBDXGb5LDg4+dKOhvRDWpCO?S$q1;5&?cJ2XPMaG|7K+IA@-H;9upc|n_uG@_au zI+6#QyQHvfB3`oUARz_(05k!S$~NY!nMhD!>w;J2+^1%Z9Ukv^7uZ|(j(Q< zE173`uZD&SU-#{iyCCcbKI$$PSVGC+_GVN~(Vh*@cBTR}yHZZn0oO!K#cQhRW{bhA zV#1xulB~^*#NoXL6yRRIW*i6Y1_i3@`^Tj{r|A98fGlt;ld z3vy!mXkTE}uFM91hn0Dx5kI;aT6`|N3}hHVOTd2~p#YzXN)yYr$LYOGdm9~~xXv#9 zaud|Hx7c36JWq@T`Qy$dH1IcrCwqqshIG)+!V?Jrr>m5F;XgP_RdNPh%L6A0W7OE1 zN#OZ44`jcjXX5513enmZJBCD+xy8Uh8KWu8e8mzAqA#ybJ-E2@6DRQ&)skQ&l2!?U z2IX2pn%_Q+$59I~YqorGG6~z!eupGwYI%A^U%oI;&2$Ow77<=7OyhWi=~Ofg8Hv`J z3<9ckrQ;Zd_)RqSpXRW$fy1)>5-;_^9R*)NJ(??i=PMC*uE{y@<#3Bep;*EZ3GQo5 zNlg#dr?5lxY1jXXL|Zcy#TVWtHl>M51?JeTNBO&73zpy)fr*%lnVYCgWu_S-pH5>r z4-T%~SS2)E5EMlHNCxsP40G&0;WC9p0*N^Do%SKyQYFlvwGfHsYRS9gw%y) z^6zXHv;R|CroB51t|Z7A3a6*?)pMlZe8r3%As8} zZOzC(0S-Y@*FlYxpvioVb+CxpcQtNc2Co~+xCPtrSdcr~h52*9P55wsWbv3IZxYz# zUXd7}l+MfsRel2_<{G1M5~#G4^N0IdZO@d+H6twmii0#_h<0MOOMQg-(@1?m zM%w-0oD0}i->qmq-%`Jr9(~-?`!klJJQ#B;hh1`s6hmXXT%i6MWoC?UzyyhERPAJc z-3x06qnkM(J>{2Rmg;3IgcJLE6G`M0o)6^82xb>LjK1&pIBm^C25o;n**gu*+25I& zT71RNMy2dFWKd1}J*6ra;4w~oc_27#{J|6-=J5^J^e1no8#OsQ2*T3=**mginnIR4 z%uT-x5^8yudbbBK+z^)P!}icZa^s^|f-o{$J2n`SayC-0MON%F-JToh!P{^<{@qh* zJ5C*Njbs02Uow_)zp-aWv$edNVV_vAKG|tx+a}2zm(WwF{2y~=aITG9UKYq@+(S7# zwg$~ah1fq$87c302xcNhFEyxNG-*vG)FJY_zfHwMm;iqpD3geoV zzr&f%R4)X3V){iwLl^^0+_(T$uCeqAuk#@Nn+ zquNdM)zk%?{Q-j>q{7F9UrNqc^4-brX*@Ig3OT2q(KQ`Uu*g*`Zeor}ESY`bv+R`8 zq?e&Yi-HuK2wl0Q_CuyJ2n-q-0`XI&mx0gJ%lg$%_!0-IYY5w*KALid8~u7}V_Pn= UdBd_1J`NjCvs6a1k8|7q12+kYT>t<8 literal 0 HcmV?d00001 diff --git a/data/images/greyT.png b/data/images/greyT.png new file mode 100644 index 0000000000000000000000000000000000000000..235e52a700fcbb5d228d9049845a12306a480508 GIT binary patch literal 11764 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 E0LIW9-T(jq literal 0 HcmV?d00001 diff --git a/data/images/pinkT.png b/data/images/pinkT.png new file mode 100644 index 0000000000000000000000000000000000000000..8580f9c5034cb929d135b1145c2c7a545c455001 GIT binary patch literal 18638 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 Ib}Rq@0Ps&HwEzGB literal 0 HcmV?d00001 diff --git a/data/images/yellowT.png b/data/images/yellowT.png new file mode 100644 index 0000000000000000000000000000000000000000..6ddb26bbbf20a8746d85f5b132c46f8a02a77878 GIT binary patch literal 23884 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 G0000IC$xY7 literal 0 HcmV?d00001 diff --git a/data/products.json b/data/products.json new file mode 100644 index 00000000..5611ed8a --- /dev/null +++ b/data/products.json @@ -0,0 +1,68 @@ +[ + { + "id": "1", + "title": "Compton T-Shirt", + "description": "Sint occaecat deserunt ex incididunt qui sint. Id minim magna labore pariatur nulla quis.", + "price": 15, + "brand": "Compton", + "category": "T-shirt", + "image": "images/yellowT.png", + "colors": ["Red", "Green", "Blue"], + "sizes": ["Small", "Medium", "Large"] + }, + { + "id": "2", + "title": "Comverges T-Shirt", + "description": "Sint occaecat deserunt ex incididunt qui sint. Id minim magna labore pariatur nulla quis.", + "price": 20, + "brand": "Comverges", + "category": "T-shirt", + "image": "images/greyT.png", + "colors": ["Red", "Green", "Blue"], + "sizes": ["Small", "Medium", "Large"] + }, + { + "id": "3", + "title": "Flexigen T-Shirt", + "description": "Sint occaecat deserunt ex incididunt qui sint. Id minim magna labore pariatur nulla quis.", + "price": 30, + "brand": "Flexigen", + "category": "T-shirt", + "image": "images/pinkT.png", + "colors": ["Red", "Green", "Blue"], + "sizes": ["Small", "Medium", "Large"] + }, + { + "id": "4", + "title": "Fuelworks T-Shirt", + "description": "Sint occaecat deserunt ex incididunt qui sint. Id minim magna labore pariatur nulla quis.", + "price": 40, + "brand": "Fuelworks", + "category": "T-shirt", + "image": "images/blackT2.png", + "colors": ["Red", "Green", "Blue"], + "sizes": ["Small", "Medium", "Large"] + }, + { + "id": "5", + "title": "Futuris T-Shirt", + "description": "Sint occaecat deserunt ex incididunt qui sint. Id minim magna labore pariatur nulla quis.", + "price": 50, + "brand": "Futuris", + "category": "T-shirt", + "image": "images/pinkT.png", + "colors": ["Red", "Green", "Blue"], + "sizes": ["Small", "Medium", "Large"] + }, + { + "id": "6", + "title": "Isoternia T-Shirt", + "description": "Sint occaecat deserunt ex incididunt qui sint. Id minim magna labore pariatur nulla quis.", + "price": 44, + "brand": "Isoternia", + "category": "T-shirt", + "image": "images/brownT.png", + "colors": ["Red", "Green", "Blue"], + "sizes": ["Small", "Medium", "Large"] + } +] diff --git a/gatsby-config.js b/gatsby-config.js index ff0ac846..3dc08f7d 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -23,6 +23,13 @@ module.exports = { author: `Google Analytics Developer Relations`, }, plugins: [ + { + resolve: `gatsby-source-filesystem`, + options: { + name: `data`, + path: `${__dirname}/data`, + }, + }, { resolve: `gatsby-plugin-manifest`, options: { @@ -79,5 +86,6 @@ module.exports = { `gatsby-plugin-image`, `gatsby-plugin-sharp`, `gatsby-transformer-sharp`, + `gatsby-transformer-json`, ], } From e0a0895c28d66e284e6e10f66a02dd6b8c2a292c Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Fri, 18 Feb 2022 13:13:00 +0000 Subject: [PATCH 16/35] Add "add to cart" button component (does not support choosing product variants yet). --- .../EnhancedEcommerce/add-to-cart.module.css | 18 ++++++++++ .../ga4/EnhancedEcommerce/add-to-cart.tsx | 36 +++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/add-to-cart.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/add-to-cart.tsx diff --git a/src/components/ga4/EnhancedEcommerce/add-to-cart.module.css b/src/components/ga4/EnhancedEcommerce/add-to-cart.module.css new file mode 100644 index 00000000..03f2531a --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/add-to-cart.module.css @@ -0,0 +1,18 @@ +.addToCart { + display: flex; + flex-direction: row; + color: var(--text-color-inverted); + background-color: var(--primary); + align-self: flex-end; + padding: var(--space-sm) var(--space-xl); + border-radius: var(--radius-md); + font-weight: var(--bold); + align-items: center; + height: var(--size-input); + justify-content: center; + transition: var(--transition); +} + +.addToCart:hover { + box-shadow: var(--shadow); +} \ No newline at end of file diff --git a/src/components/ga4/EnhancedEcommerce/add-to-cart.tsx b/src/components/ga4/EnhancedEcommerce/add-to-cart.tsx new file mode 100644 index 00000000..8727c3e5 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/add-to-cart.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import {addToCart as addToCartStyle} from "./add-to-cart.module.css" +import {StoreContext} from "./store-context" + +export function AddToCart({variantId, quantity, product}) { + const {addVariantToCart, addEvent} = React.useContext(StoreContext) + + function addToCart(e) { + e.preventDefault() + const snippet = `gtag("event", "add_to_cart", { + "currency": "USD", + "value": ${product.price * quantity}, + "items": [{ + "item_id": "${product.id}", + "item_name": "${product.title}", + "price": "${product.price}", + "item_brand": "${product.brand}", + "item_category": "${product.category}", + "index": 0, + "size": "M" + }] +});` + addEvent('add_to_cart', 'Item(s) added to cart.', snippet) + addVariantToCart(product, variantId, quantity) + } + + return ( + + ) +} \ No newline at end of file From 0804264f435f3e98fb80f562baaa48c4365a3652 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Fri, 18 Feb 2022 13:16:49 +0000 Subject: [PATCH 17/35] Add "Numeric input" component which is used to specify the product quantity on the product page. --- .../numeric-input.module.css | 100 ++++++++++++++++++ .../ga4/EnhancedEcommerce/numeric-input.tsx | 39 +++++++ 2 files changed, 139 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/numeric-input.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/numeric-input.tsx diff --git a/src/components/ga4/EnhancedEcommerce/numeric-input.module.css b/src/components/ga4/EnhancedEcommerce/numeric-input.module.css new file mode 100644 index 00000000..236489d9 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/numeric-input.module.css @@ -0,0 +1,100 @@ +.wrap { + display: inline-grid; + grid-template-columns: 1fr min-content; + grid-template-areas: "input increment" "input decrement"; + background-color: var(--input-background); + border-radius: var(--radius-md); + height: var(--size-input); + overflow: hidden; +} + +.wrap button span { + display: none; +} + +.input { + grid-area: input; + background: none; + border: none; + padding: var(--space-sm) var(--space-lg); + align-self: stretch; + width: 6ch; + border-color: var(--input-border); + border-width: 0 1px 0 0; + font-weight: var(--medium); + color: var(--input-text); +} + +.input:disabled { + color: var(--input-text-disabled); +} + +.wrap button { + background: none; + border: none; + padding: 0 var(--space-sm); + display: grid; + place-items: center; + color: var(--input-ui); +} + +.wrap button:hover { + background-color: var(--input-background-hover); + color: var(--input-ui-active); +} + +.wrap button:disabled:hover, +.wrap button:disabled { + background: none; + color: var(--input-text-disabled); +} + +.wrap button.increment { + grid-area: increment; + border-bottom: 1px var(--input-border) solid; +} + +.decrement { + grid-area: decrement; +} + +/* On mobile, make the buttons easier to press */ +@media (pointer: coarse) { + .wrap { + grid-template-columns: var(--size-input) 1fr var(--size-input); + grid-template-areas: "decrement input increment"; + } + + .input { + text-align: center; + border-width: 0 1px; + } + + .wrap button { + padding: 0 var(--space-md); + font-size: var(--text-lg); + font-weight: var(--bold); + } + + .wrap button span { + display: inline; + } + + .wrap button svg { + display: none; + } + + .wrap button.increment { + border: none; + } + + .wrap button:active { + background-color: var(--input-background-hover); + color: var(--input-ui-active); + } + + .wrap button:hover { + background-color: inherit; + color: inherit; + } +} \ No newline at end of file diff --git a/src/components/ga4/EnhancedEcommerce/numeric-input.tsx b/src/components/ga4/EnhancedEcommerce/numeric-input.tsx new file mode 100644 index 00000000..f2e9e80c --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/numeric-input.tsx @@ -0,0 +1,39 @@ +import * as React from "react" +import {MdArrowDropDown, MdArrowDropUp} from "react-icons/md" +import {decrement, increment, input, wrap} from "./numeric-input.module.css" + +export function NumericInput({ + onIncrement, + onDecrement, + disabled, + ...props + }) { + return ( +
+ + + +
+ ) +} \ No newline at end of file From e18fad16a527509c03856f65c18925098f98d4a1 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Fri, 18 Feb 2022 20:24:24 +0000 Subject: [PATCH 18/35] Add "product card" component which displays the product details. --- .../EnhancedEcommerce/product-card.module.css | 31 +++++++++++ .../ga4/EnhancedEcommerce/product-card.tsx | 54 +++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/product-card.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/product-card.tsx diff --git a/src/components/ga4/EnhancedEcommerce/product-card.module.css b/src/components/ga4/EnhancedEcommerce/product-card.module.css new file mode 100644 index 00000000..6358089e --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/product-card.module.css @@ -0,0 +1,31 @@ +.productCardStyle { + max-width: 400px; + cursor: pointer; + text-decoration: none; + padding-bottom: var(--space-md); +} + +.productImageStyle { + margin-bottom: var(--space-md); +} + +.productDetailsStyle { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + font-weight: var(--semibold); +} + +.productHeadingStyle { + width: 100%; + font-size: var(--text-lg); + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + line-height: var(--dense); +} + +.productPrice { + color: var(--text-color-secondary); +} \ No newline at end of file diff --git a/src/components/ga4/EnhancedEcommerce/product-card.tsx b/src/components/ga4/EnhancedEcommerce/product-card.tsx new file mode 100644 index 00000000..2b9e7134 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/product-card.tsx @@ -0,0 +1,54 @@ +import * as React from "react" +import {Link} from "gatsby" +import {GatsbyImage} from "gatsby-plugin-image" + +import {productCardStyle, productDetailsStyle, productHeadingStyle, productImageStyle, productPrice,} from "./product-card.module.css" +import {StoreContext} from "@/components/ga4/EnhancedEcommerce/store-context"; + +export function ProductCard({product}) { + const { + title, + image, + slug, + price, + } = product + const {addEvent} = React.useContext(StoreContext) + + function selectProduct() { + const snippet = `gtag("event", "select_item", { + "currency": "USD", + "item_list_name": "homepage", + "item_list_id": "homepage", + "items": [{ + "item_id": "${product.id}", + "item_name": "${product.title}", + "price": "${product.price}", + "item_brand": "${product.brand}", + "item_category": "${product.category}", + "index": "${product.id}", + }] +});` + addEvent('select_item', 'An item was selected from a list.', snippet) + } + + return ( + +
+ +
+ +
+

+ {title} +

+
${price}
+
+ + ) +} From 23170863eee7227d161fe25bd7bb83a4db014846 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Fri, 18 Feb 2022 20:27:28 +0000 Subject: [PATCH 19/35] Add "product listing" component which displays the grid of products. --- .../product-listing.module.css | 7 ++++ .../ga4/EnhancedEcommerce/product-listing.tsx | 39 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 src/components/ga4/EnhancedEcommerce/product-listing.module.css create mode 100644 src/components/ga4/EnhancedEcommerce/product-listing.tsx diff --git a/src/components/ga4/EnhancedEcommerce/product-listing.module.css b/src/components/ga4/EnhancedEcommerce/product-listing.module.css new file mode 100644 index 00000000..40d76849 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/product-listing.module.css @@ -0,0 +1,7 @@ +.listingContainerStyle { + display: grid; + grid-template-columns: var(--product-grid); + place-items: center; + gap: var(--size-gutter-raw); + padding: 0 var(--size-gutter-raw); +} \ No newline at end of file diff --git a/src/components/ga4/EnhancedEcommerce/product-listing.tsx b/src/components/ga4/EnhancedEcommerce/product-listing.tsx new file mode 100644 index 00000000..318741d5 --- /dev/null +++ b/src/components/ga4/EnhancedEcommerce/product-listing.tsx @@ -0,0 +1,39 @@ +import * as React from "react" +import {ProductCard} from "./product-card" +import {listingContainerStyle} from "./product-listing.module.css" +import {StoreContext, Product} from "@/components/ga4/EnhancedEcommerce/store-context"; + +interface ProductListingProps { + products: Product[] +} + +export function ProductListing({products}: ProductListingProps) { + const {addEvent} = React.useContext(StoreContext) + + React.useEffect(() => { + const items = products.map( (product, i) => { + return ` { + "item_id": "${product.id}", + "item_name": "${product.title}", + "price": "${product.price}", + "item_brand": "${product.brand}", + "item_category": "${product.category}", + "list_name": "Search Results", + "list_position": ${i}, + }` + }).join(",\n") + const snippet = `gtag("event", "view_item_list", { + "items": [${items}] +});` + addEvent('view_item_list', 'View items list (search result).', snippet) + + + }); + return ( +
+ {products.map((p) => ( + + ))} +
+ ) +} From 4a4078c8ead50b95d099ed512aa2bd04a824dc43 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Fri, 18 Feb 2022 20:28:00 +0000 Subject: [PATCH 20/35] declare external interfaces --- src/components/ga4/EnhancedEcommerce/store-context.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ga4/EnhancedEcommerce/store-context.tsx b/src/components/ga4/EnhancedEcommerce/store-context.tsx index cec54127..5c1637c1 100644 --- a/src/components/ga4/EnhancedEcommerce/store-context.tsx +++ b/src/components/ga4/EnhancedEcommerce/store-context.tsx @@ -9,7 +9,7 @@ interface GAEvent snippet: string } -interface Product +export interface Product { id: number title: string @@ -18,7 +18,7 @@ interface Product price: number } -interface CartItem +export interface CartItem { id: number product: Product From c761d2ff926978a724e93be22658ff311e59532e Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Fri, 18 Feb 2022 20:29:54 +0000 Subject: [PATCH 21/35] add Product Details page --- .../products/product-page.module.css | 179 ++++++++++++++++++ .../products/{ProductsJson.title}.tsx | 103 ++++++++++ 2 files changed, 282 insertions(+) create mode 100644 src/pages/ga4/enhanced-ecommerce/products/product-page.module.css create mode 100644 src/pages/ga4/enhanced-ecommerce/products/{ProductsJson.title}.tsx diff --git a/src/pages/ga4/enhanced-ecommerce/products/product-page.module.css b/src/pages/ga4/enhanced-ecommerce/products/product-page.module.css new file mode 100644 index 00000000..28b61095 --- /dev/null +++ b/src/pages/ga4/enhanced-ecommerce/products/product-page.module.css @@ -0,0 +1,179 @@ +.productBox { + display: grid; + grid-template-columns: 1fr; + column-gap: var(--space-3xl); +} + +.container { + padding: var(--size-gutter-raw); +} + +.header { + font-size: var(--text-display); + font-weight: var(--bold); + margin-bottom: var(--space-xl); + line-height: var(--dense); +} + +.productDescription { + font-size: var(--text-prose); +} + +.productImageWrapper { + position: relative; + padding-bottom: var(--space-2xl); +} + +.productImageList { + display: flex; + overflow-x: auto; +} + +.productImageListItem { + display: flex; + flex: 0 0 100%; + white-space: nowrap; +} + +.scrollForMore { + text-align: center; + margin-top: 1rem; + display: none; + font-size: var(--text-lg); + left: 50%; + position: absolute; +} + +.noImagePreview { + display: flex; + align-items: center; + justify-content: center; + min-height: 300px; + font-size: var(--text-lg); +} + +.priceValue > span { + font-size: var(--text-display); + font-weight: var(--bold); + line-height: var(--dense); + color: var(--primary); +} + +.priceValue { + padding: var(--space-lg) 0; +} + +.visuallyHidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.optionsWrapper { + display: grid; + grid-template-columns: var(--product-grid); + gap: var(--space-lg); + padding-bottom: var(--space-lg); +} + +.addToCartStyle { + display: grid; + grid-template-columns: min-content 1fr; + gap: var(--space-lg); +} + +.selectVariant { + background-color: var(--input-background); + border-radius: var(--radius-md); + cursor: pointer; + margin-top: var(--space-md); + min-width: 24ch; + position: relative; +} + +.selectVariant select { + appearance: none; + background-color: transparent; + border: none; + color: var(--input-text); + cursor: inherit; + font-size: var(--text-md); + font-weight: var(--medium); + height: var(--size-input); + margin: 0; + padding: var(--space-sm) var(--space-lg); + padding-right: var(--space-2xl); + width: 100%; +} + +.selectVariant::after { + background-image: url("data:image/svg+xml,%3Csvg fill='none' height='8' viewBox='0 0 13 8' width='13' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='m6.87794 7.56356c-.19939.23023-.55654.23024-.75593 0l-5.400738-6.23623c-.280438-.32383-.050412-.82733.377968-.82733h10.80146c.4284 0 .6584.5035.378.82733z' fill='%2378757a'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + content: ""; + height: 8px; + position: absolute; + right: var(--space-lg); + top: 50%; + transform: translateY(-50%); + width: 13px; + pointer-events: none; +} + +.labelFont { + font-size: var(--space-lg); + line-height: var(--space-xl); + padding-right: var(--space-md); + color: var(--text-color-secondary); +} + +.tagList a { + font-weight: var(--semibold); + color: var(--text-color-secondary); + padding-right: var(--space-md); +} + +.tagList a:hover { + color: var(--text-color); + text-decoration: underline; +} + +.breadcrumb { + color: var(--text-color-secondary); + font-size: var(--text-sm); + display: flex; + align-items: center; + flex-direction: row; +} + +.breadcrumb a:hover { + color: var(--text-color); + text-decoration: underline; +} + +.metaSection { + padding-top: var(--space-3xl); + display: grid; + grid-template-columns: max-content 1fr; + align-items: baseline; +} + +@media (min-width: 640px) { + .productBox { + grid-template-columns: 1fr 2fr; + } + .addToCartStyle { + grid-template-columns: min-content max-content; + } +} + +@media (min-width: 1024px) { + .productBox { + grid-template-columns: repeat(2, 1fr); + } +} \ No newline at end of file diff --git a/src/pages/ga4/enhanced-ecommerce/products/{ProductsJson.title}.tsx b/src/pages/ga4/enhanced-ecommerce/products/{ProductsJson.title}.tsx new file mode 100644 index 00000000..a0891c9e --- /dev/null +++ b/src/pages/ga4/enhanced-ecommerce/products/{ProductsJson.title}.tsx @@ -0,0 +1,103 @@ +import * as React from "react" +import {graphql} from "gatsby" + +import Layout from "@/components/Layout" +import {Header} from "@/components/ga4/EnhancedEcommerce/header"; +import {Footer} from "@/components/ga4/EnhancedEcommerce/footer"; +import {AddToCart} from "@/components/ga4/EnhancedEcommerce/add-to-cart" +import {NumericInput} from "@/components/ga4/EnhancedEcommerce/numeric-input" +import {GatsbyImage} from "gatsby-plugin-image"; +import { + productBox, + container, + header, + productImageWrapper, + priceValue, + addToCartStyle, + productDescription, +} from "./product-page.module.css" + +export default ({location: {pathname}, data: {productsJson}}) => { + const { + price, + title, + description, + image, + } = productsJson + const initialVariant = ['Black', 'M'] + const [variant, setVariant] = React.useState({...initialVariant}) + const productVariant = variant; + const [quantity, setQuantity] = React.useState(1) + return ( + +
+
+
+
+
+ +
+
+
+

{title}

+

{description}

+

+ ${price} +

+
+ setQuantity((q) => Math.min(q + 1, 20))} + onDecrement={() => setQuantity((q) => Math.max(1, q - 1))} + onChange={(event) => setQuantity(event.currentTarget.value)} + value={quantity} + disabled={false} + min="1" + max="20" + /> + +
+
+
+
+