diff --git a/blog/ca/4_enginyeria-inversa-del-ecommerce-de-nike.mdx b/blog/ca/4_enginyeria-inversa-del-ecommerce-de-nike.mdx new file mode 100644 index 0000000..a5efd24 --- /dev/null +++ b/blog/ca/4_enginyeria-inversa-del-ecommerce-de-nike.mdx @@ -0,0 +1,1136 @@ +--- +title: Enginyeria inversa del ecommerce de Nike amb només el navegador +description: Article sobre les tecnologies que s'utilitzen a l'ecommerce de nike +image: /assets/blog/descubrint-el-ecommerce-de-nike/cover-image.jpg +published: true +publishedDate: 2023-12-01 +authors: eudago +seo: + metatitle: > + Enginyeria inversa del ecommerce de Nike amb només el navegador + metadescription: > + Article sobre les tecnologies que s'utilitzen a l'ecommerce de nike + image: > + /assets/blog/descubrint-el-ecommerce-de-nike/cover-image.jpg +--- + +El proposit d'aquest article es veure fins al punt que es pugui quines tecnologies utilitza i com les utiltiza un ecommerce "gran" com en aquest cas es el de nike i fer-ho utilitzant les developer tools del navegador. + +Primer de tot si inspeccionem la pàgina podem veure que utilitza el framework [Next.js](https://nextjs.org/), + +```html {1,3-4} +
+
+
+
+ +
+
+
+
+``` + +On podem trobar el div amb l'id `__next`, això o que en uns dels primer showcases que podem veure a la propia web de nextjs. + +
+ + +
+

Dins del la pestanya sources dels devtools del navegador podem veure que hi ha la "carpeta" webpack://, aquesta carpeta es una carpeta virtual que ens permet veure els fitxers originals, ja que tenen habilitada l'opció de source maps a la configuració de webpack (cosa una mica rara tot sigui dit).

+

+

+ També podem veure la carpeteta de webpack dins d'altres carpetes, com ara són les de _N_E, analytics-client, privacy-consent, privacy-Core i WebShellClient etc. + Això ens facilitara la vida a l'hora de veure de que tractent ja que no haurem de tractar amb codi minificat ni uglificat. +

+
+ +
+ +També podem usar les [react devtools](https://react.dev/learn/react-developer-tools) per poder veure les props i els estats dels components que es renderitzen a la pàgina: + +![reat-devtocols](/assets/blog/descubrint-el-ecommerce-de-nike/react-devtools.PNG) + +Si analitzem una mica podem veure que estan utilitzan Redux per la gestió de l'estat de l'aplicació, el cual es carrega al primer component de la pagina, aqui tenim un [tutorial](https://blog.logrocket.com/use-redux-next-js/) de com s'utilitza redux amb nextjs. + +Al primer component de la pàgina també podem veure les rutes dins de nextjs, per exemple la home és: + +```json +isSsr:true +query: { + "page": [ + "es", + "ca" + ] +} +route: "/[[...page]]" +``` + +En canvi a la url de les ofertes amb path **/es/ca/w/ofertes-3yaep**: + +```json +isSsr:true +query: { + country: "es", + language: "ca", + slug: ["ofertes-3yaep"] +} +route:"/[country]/[language]/w/[[...slug]]" +``` + +O un producte amb path **/es/ca/t/dri-fit-samarreta-de-fitnes-home-v75KxD/AR6029-063**: + +```json +isSsr:true +query: { + slug: "dri-fit-samarreta-de-fitnes-home-v75KxD", + styleColor: "AR6029-063", +} +route:"/t/[slug]/[styleColor]" +``` + +Una de les coses que podem treure de les propietats que rep el primer component es el tipus de renderitzat que utiltizen amb el nextJs. + +```json +__N_SSP: true +__N_SSG: undefined +``` + +Podem veure que aquestes pagines es renderitzen amb server side rendering (SSR), i dins del +server side rendering podem veure que no fan servir [server side generation (SSG)](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation), si no que les pagines són generades a partir del [getServerSideProp (SSP)](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props), +això segurament es per la cantitat de productes que tenen i per mantenir les pagines mes actualitzades. + +En canvi la pagina de perfil (/es/ca/member/profile/) si que utilitza el SSG, tot i que realment el contingut rellevant es carrega amb el client side rendering (CSR). + +```json +__N_SSG: true +route:"/[country]/[language]/member/profile" +``` + +També podem analitzar la cache de les diferentes pagines mirant el Cache-Control dels headers de resposta a la primera petició: + + + + + + + + + + + + + + + + + + + + + + +
URLCache-Control
/es/ca/max-age=646
/es/ca/w/ofertes-3yaepmax-age=287
/es/ca/t/dri-fit-samarreta-de-fitnes-home-v75KxD/AR6029-063max-age=899
+ +Del que podem veure que la home es refresca cada 10 minuts, les ofertes cada 5 minuts i els productes cada 15 minuts, pero el tema del caching millor el deixem per un futur article. + +Si mirem les peticiones un dels headers que portla la resposta es el de **Akamai-Grn**, si busquem akami pel codi o directament a google podem veure que es tractara d'alguns serveis de l'empresa [Akami](https://en.wikipedia.org/wiki/Akamai_Technologies) com ara cyberseguretat, cloud services o CDN entre molts altres. + +```js +import { useRef } from 'react'; +import { useQuery } from 'react-query'; +import { fetchClient } from '@nike/fetch-client'; +import { getExperimentDriver } from '@nike/ux-tread-optimizely'; +import useOptimizelyEnabled from './use-optimizely-enabled'; + +export const PROD_OPTIMIZELY_URL = `https://${process.env.NEXT_PUBLIC_HOST_NAME}/assets/vendor/optimizely/prod/5aUCtZvFuDLjvdqdf2w53M.json`; +export const DEV_OPTIMIZELY_URL = `https://assets.commerce.nikecloud.com/vendor/optimizely/test/3srgubLSVD2VBXqgkJtCRh.json`; // NOTE - we need to use this for now until akamai is fixed +export const OPTIMIZELY_ERROR_MESSAGE = 'Error fetching optimizely datafile'; + +const useOptimizelyDataFile = () => { + const optimizelyEnabled = useOptimizelyEnabled(); + const optimizelyClient = useRef(); + const isProd = process.env.NODE_ENV === 'production'; + const optimizelyUrl = isProd ? PROD_OPTIMIZELY_URL : DEV_OPTIMIZELY_URL; + + const { data } = useQuery( + optimizelyUrl, + () => + fetchClient(optimizelyUrl, { method: 'GET' }, OPTIMIZELY_ERROR_MESSAGE), + { enabled: optimizelyEnabled } + ); + + if (data) { + optimizelyClient.current = getExperimentDriver(data); + } + + return { datafile: data, optimizelyClient: optimizelyClient }; +}; + +export default useOptimizelyDataFile; +``` + +En aquest cas sembla que es tracta d'un servei de CDN. + +## Design system + +Si indaguem una mica per respondre a la pregunta de com gestionen els estils per els components de react, veiem que en alguns dels components s'utilitza [emotion](https://emotion.sh/docs/introduction), una llibreria css-in-js: + +```js +import styled from '@emotion/styled'; +``` + +A part podem veure diferents paquets que fan referencia a un design system com ara el **@nike/nike-design-system-components** on podem veure que tenen diferents components: + +- Accordion +- Buttons +- Cards/ProductCard +- Carousel +- Details +- **Layout** + +Dins de layout hi han diferents components per gestionar el layout de la pàgina, com ara el AspectRation, Box, Grid, Stack, ... + +També podem trobar quin son els espais que utilitzent: + +```js +export var getSpacing = function (t) { + var n = 'var(--podium-cds-size-spacing-'.concat(t.toLowerCase(), ')'); + return ['xs', 's', 'm', 'l', 'xl', 'xxl', 'xxxl', 'xxxxl'].includes(t) + ? n + : t; +}; +``` + +On els valors de les variables són: + +```css +--podium-cds-size-spacing-xs: 4px; +--podium-cds-size-spacing-s: 8px; +--podium-cds-size-spacing-m: 12px; +--podium-cds-size-spacing-l: 24px; +--podium-cds-size-spacing-xl: 36px; +--podium-cds-size-spacing-xxl: 60px; +--podium-cds-size-spacing-xxxl: 84px; +--podium-cds-size-spacing-xxxxl: 120px; +``` + +- Loaders/Skeleton +- NikeDesignSystemProvider +- PullQuote +- SelectionControls/Switch +- SizeChart +- **Typography/Text** + +Aqui podem trobar un mapping depenent del tipus de text quin element de html es renderitzara, per temes de seo: + +```js +export var componentMapping = { + body1: 'p', + body2: 'p', + body3: 'p', + body1Strong: 'p', + body2Strong: 'p', + body3Strong: 'p', + legal: 'p', + editorialBody1: 'p', + editorialBody1Strong: 'p', + oversize1: 'p', + oversize2: 'p', + oversize3: 'p', + display1: 'h1', + display2: 'h2', + display3: 'h3', + display4: 'h4', + title1: 'h1', + title2: 'h2', + title3: 'h3', + title4: 'h4', + conversation1: 'p', + conversation2: 'p', + conversation3: 'p', + conversation4: 'p', +}; +``` + +Podem fixar-nos que en alguns components fan us dels tokens importats de `@nike/design-system-base`, com podem veure al fitxer components/dialog/styles.js: + +```js +import styled from '@emotion/styled'; +import { Modal } from '@nike/nr-sole-modal'; +import tokens from '@nike/design-system-base'; + +export const StyledModal = styled(Modal)` + @media only screen and (max-width: ${tokens.bp('tablet')}px) { + transition: visibility 0s ease, bottom 0.5s ease; + } + & .modal-container { + border-radius: 16px; + padding: 24px; + text-align: left; + transform: translateY(50px); + transition: opacity 0.6s ease 0.2s, transform 0.4s ease 0.2s, + height 0.4s ease; + padding: 12px; + + /* desktop media query minus one for ipad pro */ + @media only screen and (max-width: ${tokens.bp('desktop') - 1}px) { + &.modal-container { + max-width: 100%; + position: fixed; + padding: 16px 0 0; + bottom: 0px; + border-radius: 0; + } + } + } +`; +``` + +d'aqui podem extreure els tokens: + +```json +{ + "default": { + "opts": { + "fontSizeUnit": "px" + }, + "ds": { + "type": { + "baseFontSize": "16px", + "sizes": { + "xs": "14px", + "sm": "20px", + "baseline": "16px", + "md": "22px", + "lg": "26px", + "xl": "30px" + }, + "size": { + "brandMarketing": { + "xs": "14px", + "sm": "20px", + "baseline": "16px", + "md": "60px", + "lg": "80px", + "xl": "120px" + }, + "desktop": { + "xs": "14px", + "sm": "20px", + "baseline": "16px", + "md": "24px", + "lg": "28px", + "xl": "36px" + } + }, + "fontFamily": { + "base": "\"Helvetica Neue\", Helvetica, Arial, sans-serif", + "brand": "\"Nike TG\", \"Helvetica Neue\", Helvetica, Arial, sans-serif", + "marketing": "\"Nike Futura\", \"Helvetica Neue\", Helvetica, Arial, sans-serif" + }, + "lineHeight": { + "mobile": { + "14": 1.7142857142857142, + "16": 1.75, + "20": 1.5, + "22": 1.4545454545454546, + "24": 1.5, + "26": 1.3076923076923077, + "28": 1.4285714285714286, + "30": 1.3333333333333333, + "36": 1.3333333333333333, + "40": 0.9, + "60": 0.8333333333333334, + "80": 0.875, + "120": 0.8333333333333334 + }, + "tablet": { + "20": 1.6, + "60": 0.9333333333333333 + } + }, + "fontWeight": { + "regular": 400, + "medium": 500 + } + }, + "colors": { + "colorPalette": { + "black": { + "base": "#111111", + "light": "rgba(0, 0, 0, 0.75)", + "dark": "#0D0D0D" + }, + "white": { + "base": "#ffffff", + "light": "rgba(255, 255, 255, 0.75)", + "dark": "#BFBFBF" + }, + "grey": { + "base": "#757575", + "light": "rgba(117, 117, 117, 0.75)", + "dark": "#585858" + }, + "orange": { + "base": "#FA5400", + "light": "rgba(250, 84, 0, 0.75)", + "dark": "#BC3F00" + }, + "red": { + "base": "#d43f21" + }, + "green": { + "base": "#128a09" + } + }, + "brand": { + "black": "#111111", + "white": "#ffffff", + "grey": "#757575", + "orange": "#FA5400", + "red": "#d43f21", + "green": "#128a09", + "inactiveGrey": "#CCCCCC", + "borderGrey": "#E5E5E5", + "primaryGrey": "#F5F5F5", + "secondaryGrey": "#FAFAFA", + "accent": "#FA5400", + "error": "#d43f21", + "success": "#128a09" + } + }, + "breakpoints": { + "mobile": 0, + "tablet": 600, + "desktop": 960, + "largeDesktop": 1440, + "extraLargeDesktop": 1920 + }, + "mediaQueries": { + "mobile": "only screen and (min-width: 0px)", + "sm": "(max-width: 599px)", + "md": "(min-width: 600px) and (max-width: 959px)", + "lg": "(min-width: 960px) and (max-width: 1439px)", + "xl": "(min-width: 1440px) and (max-width: 1919px)", + "xxl": "(min-width: 1920px)", + "tablet": "only screen and (min-width: 600px)", + "desktop": "only screen and (min-width: 960px)", + "largeDesktop": "only screen and (min-width: 1440px)", + "extraLargeDesktop": "only screen and (min-width: 1920px)" + }, + "zIndex": { + "z0": 0, + "z1": 1, + "z2": 2, + "z3": 3, + "z4": 4, + "z5": 5, + "z6": 6, + "z7": 7, + "z8": 8, + "z9": 9, + "z10": 10, + "low": 0, + "mid": 5, + "high": 10 + }, + "scaleIncrement": 4, + "spacing": { + "baseline": 16, + "padding": "4px", + "scale": [ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, + 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116 + ] + }, + "layout": { + "gutter": 16, + "maxWidth": 1808, + "grid": { + "columnCount": 12, + "columnWidth": { + "1": "8.333333333333334%", + "2": "16.666666666666668%", + "3": "25%", + "4": "33.333333333333336%", + "5": "41.66666666666667%", + "6": "50%", + "7": "58.333333333333336%", + "8": "66.66666666666667%", + "9": "75%", + "10": "83.33333333333334%", + "11": "91.66666666666667%", + "12": "100%" + } + } + }, + "transition": { + "default": { + "duration": "250ms", + "timing": "cubic-bezier(0.77, 0, 0.175, 1)", + "transition": "all 250ms cubic-bezier(0.77, 0, 0.175, 1)" + } + }, + "borderRadius": 24 + } + } +} +``` + +Colors: + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +### Internacionalització + +Si mirem el network ja podem veure algunes peticions que porten el i18n al seu path, aquest és el cas de la pàgina de favorits (https://www.nike.com/favorites): + +***https://www.nike.com/assets/i18n/dotcom-fragments-recommendations/en-US.json*** + +On la resposta es un json amb una key que porta un objecte amb les keys de "value" i "description": + +```json +{ + "favorites.productRecommendationsCarousel.title": { + "value": "You Might Also Like", + "description": "The title for the product recommendations carousel displayed on the favorites page" + }, + "profile.favoritesCarousel.title": { + "value": "Favorites", + "description": "Title for the favorites section" + }, + "profile.favoritesCarousel.viewAllLink": { + "value": "View All", + "description": "The link that takes users to the page where they can view all of their Favorites" + }, + "recommendations.badges.member_access_label": { + "value": "Member Access", + "description": "This text is displayed in carousel card for member exclusive product" + }, + "recommendations.badges.member_exclusive": { + "value": "Get this product with your free Nike Membership Profile", + "description": "This text is displayed in carousel card for member exclusive product" + }, + "recommendations.badges.nike_by_you": { + "value": "Nike by You", + "description": "This text is displayed in carousel card for Nike by You product" + }, + "recommendations.badges.nike_by_you_label": { + "value": "Customize", + "description": "This text is displayed in carousel card for Nike by You product" + }, + "recommendations.favorites.title": { + "value": "Find Your Next Favorite", + "description": "Default Title of Recommendations" + }, + "recommendations.next.label": { + "value": "Next Recommended Product", + "description": "Next Button Title" + }, + "recommendations.nullsearch.title": { + "value": "These popular items might interest you", + "description": "Product wall null search title for recommendations carousel" + }, + "recommendations.previous.label": { + "value": "Previous Recommended Product", + "description": "Previous Product Label for Button" + }, + "recommendations.top_trending": { + "value": "Top Trending", + "description": "Title for a component which will display our top trending products for user's market place" + } +} +``` + +webpack://Recommendations Carousel/src/constants/translations.ts + +També podem veure com dins del script amb el tag ` + + + + +``` + +The `
` element with the ID \_\_next can be found, which is often featured in one of the primary showcases on the Next.js official website itself. + +
+ + +
+

Within the "Sources" tab of the browser's dev tools, we can observe the presence of the "folder" webpack://. This folder represents a virtual directory that allows us to view the original files. It's made possible due to the enabled source maps option in the webpack configuration (which, it's worth mentioning, is a somewhat uncommon occurrence).

+

+

+ We can also locate the webpack folder within other directories, such as _N_E, analytics-client, privacy-consent, privacy-Core, and WebShellClient, among others. This greatly simplifies our task when examining the content, as we won't have to deal with minified or obfuscated code. +

+
+ +
+ +We can also utilize the [react devtools](https://react.dev/learn/react-developer-tools) to inspect the props and states of the components rendered on the page: + +![reat-devtocols](/assets/blog/descubrint-el-ecommerce-de-nike/react-devtools.PNG) + +Upon analysis, it's evident that they are employing Redux for managing the application state, which gets loaded into the initial component of the page. Here's a helpful tutorial on using Redux with Next.js. + +Within the initial page component, we can also identify the routes used in Next.js. For instance, the home route is: + +```json +isSsr:true +query: { + "page": [ + "es", + "ca" + ] +} +route: "/[[...page]]" +``` + +Instead to the url of the offers with path **/es/ca/w/ofertes-3yaep**: + +```json +isSsr:true +query: { + country: "es", + language: "ca", + slug: ["ofertes-3yaep"] +} +route:"/[country]/[language]/w/[[...slug]]" +``` + +Or a product with path **/es/ca/t/dri-fit-fitness-shirt-men-v75KxD/AR6029-063**: + +```json +isSsr:true +query: { + slug: "dri-fit-samarreta-de-fitnes-home-v75KxD", + styleColor: "AR6029-063", +} +route:"/t/[slug]/[styleColor]" +``` + +One of the things we can extract from the properties received by the first component is the type of rendering they are using with Next.js. + +```json +__N_SSP: true +__N_SSG: undefined +``` + +We can observe that these pages are rendered using server-side rendering ([SSR](https://nextjs.org/docs/pages/building-your-application/rendering/static-site-generation)), and within server-side rendering, they do not utilize server-side generation (SSG). Instead, the pages are generated using getServerSideProps ([SSP](https://nextjs.org/docs/pages/building-your-application/data-fetching/get-server-side-props)). This is likely due to the volume of products they have and the need to keep the pages more up-to-date. + +However, the profile page (/es/ca/member/profile/) does utilize SSG, although the relevant content is actually loaded using client-side rendering (CSR). + +```json +__N_SSG: true +route:"/[country]/[language]/member/profile" +``` + +Certainly! We can analyze the cache of the different pages by inspecting the `Cache-Control` headers in the response of the initial request: + + + + + + + + + + + + + + + + + + + + + + +
URLCache-Control
/es/ca/max-age=646
/es/ca/w/ofertes-3yaepmax-age=287
/es/ca/t/dri-fit-samarreta-de-fitnes-home-v75KxD/AR6029-063max-age=899
+ +From what we can see, the home page refreshes every 10 minutes, the offers update every 5 minutes, and the products refresh every 15 minutes. However, delving deeper into caching is something we might reserve for a future article. + +Looking into the requests, one of the response headers is **Akamai-Grn**. If we search for Akamai in the code or directly on Google, we can find that it relates to various services offered by [Akami](https://en.wikipedia.org/wiki/Akamai_Technologies), such as cybersecurity, cloud services, or content delivery network (CDN), among many others. + +```js +import { useRef } from 'react'; +import { useQuery } from 'react-query'; +import { fetchClient } from '@nike/fetch-client'; +import { getExperimentDriver } from '@nike/ux-tread-optimizely'; +import useOptimizelyEnabled from './use-optimizely-enabled'; + +export const PROD_OPTIMIZELY_URL = `https://${process.env.NEXT_PUBLIC_HOST_NAME}/assets/vendor/optimizely/prod/5aUCtZvFuDLjvdqdf2w53M.json`; +export const DEV_OPTIMIZELY_URL = `https://assets.commerce.nikecloud.com/vendor/optimizely/test/3srgubLSVD2VBXqgkJtCRh.json`; // NOTE - we need to use this for now until akamai is fixed +export const OPTIMIZELY_ERROR_MESSAGE = 'Error fetching optimizely datafile'; + +const useOptimizelyDataFile = () => { + const optimizelyEnabled = useOptimizelyEnabled(); + const optimizelyClient = useRef(); + const isProd = process.env.NODE_ENV === 'production'; + const optimizelyUrl = isProd ? PROD_OPTIMIZELY_URL : DEV_OPTIMIZELY_URL; + + const { data } = useQuery( + optimizelyUrl, + () => + fetchClient(optimizelyUrl, { method: 'GET' }, OPTIMIZELY_ERROR_MESSAGE), + { enabled: optimizelyEnabled } + ); + + if (data) { + optimizelyClient.current = getExperimentDriver(data); + } + + return { datafile: data, optimizelyClient: optimizelyClient }; +}; + +export default useOptimizelyDataFile; +``` + +In this case it appears to be a CDN service. + +## Design system + +If we delve a bit deeper to answer the question of how they manage styles for React components, it seems that some of the components utilize [Emotion](https://emotion.sh/docs/introduction), a CSS-in-JS library. + +```js +import styled from '@emotion/styled'; +``` + +Aside from that, we can see various packages that reference a design system like **@nike/nike-design-system-components** where there are different components listed: + +- Accordion +- Buttons +- Cards/ProductCard +- Carousel +- Details +- **Layout** + +Within the layout section, there are different components to manage the page layout, such as AspectRatio, Box, Grid, Stack, and more. + +We can also find information about the spaces they use: + +```js +export var getSpacing = function (t) { + var n = 'var(--podium-cds-size-spacing-'.concat(t.toLowerCase(), ')'); + return ['xs', 's', 'm', 'l', 'xl', 'xxl', 'xxxl', 'xxxxl'].includes(t) + ? n + : t; +}; +``` + +Where the values of the variables are: + +```css +--podium-cds-size-spacing-xs: 4px; +--podium-cds-size-spacing-s: 8px; +--podium-cds-size-spacing-m: 12px; +--podium-cds-size-spacing-l: 24px; +--podium-cds-size-spacing-xl: 36px; +--podium-cds-size-spacing-xxl: 60px; +--podium-cds-size-spacing-xxxl: 84px; +--podium-cds-size-spacing-xxxxl: 120px; +``` + +- Loaders/Skeleton +- NikeDesignSystemProvider +- PullQuote +- SelectionControls/Switch +- SizeChart +- **Typography/Text** + +Here, we can find a mapping that determines which HTML element will be rendered depending on the type of text, aiming for SEO considerations: + +```js +export var componentMapping = { + body1: 'p', + body2: 'p', + body3: 'p', + body1Strong: 'p', + body2Strong: 'p', + body3Strong: 'p', + legal: 'p', + editorialBody1: 'p', + editorialBody1Strong: 'p', + oversize1: 'p', + oversize2: 'p', + oversize3: 'p', + display1: 'h1', + display2: 'h2', + display3: 'h3', + display4: 'h4', + title1: 'h1', + title2: 'h2', + title3: 'h3', + title4: 'h4', + conversation1: 'p', + conversation2: 'p', + conversation3: 'p', + conversation4: 'p', +}; +``` + +We can notice that in some components, they make use of tokens imported from @nike/design-system-base, as we can see in the file components/dialog/styles.js: + +```js +import styled from '@emotion/styled'; +import { Modal } from '@nike/nr-sole-modal'; +import tokens from '@nike/design-system-base'; + +export const StyledModal = styled(Modal)` + @media only screen and (max-width: ${tokens.bp('tablet')}px) { + transition: visibility 0s ease, bottom 0.5s ease; + } + & .modal-container { + border-radius: 16px; + padding: 24px; + text-align: left; + transform: translateY(50px); + transition: opacity 0.6s ease 0.2s, transform 0.4s ease 0.2s, + height 0.4s ease; + padding: 12px; + + /* desktop media query minus one for ipad pro */ + @media only screen and (max-width: ${tokens.bp('desktop') - 1}px) { + &.modal-container { + max-width: 100%; + position: fixed; + padding: 16px 0 0; + bottom: 0px; + border-radius: 0; + } + } + } +`; +``` + +From here, we can extract the tokens: + +```json +{ + "default": { + "opts": { + "fontSizeUnit": "px" + }, + "ds": { + "type": { + "baseFontSize": "16px", + "sizes": { + "xs": "14px", + "sm": "20px", + "baseline": "16px", + "md": "22px", + "lg": "26px", + "xl": "30px" + }, + "size": { + "brandMarketing": { + "xs": "14px", + "sm": "20px", + "baseline": "16px", + "md": "60px", + "lg": "80px", + "xl": "120px" + }, + "desktop": { + "xs": "14px", + "sm": "20px", + "baseline": "16px", + "md": "24px", + "lg": "28px", + "xl": "36px" + } + }, + "fontFamily": { + "base": "\"Helvetica Neue\", Helvetica, Arial, sans-serif", + "brand": "\"Nike TG\", \"Helvetica Neue\", Helvetica, Arial, sans-serif", + "marketing": "\"Nike Futura\", \"Helvetica Neue\", Helvetica, Arial, sans-serif" + }, + "lineHeight": { + "mobile": { + "14": 1.7142857142857142, + "16": 1.75, + "20": 1.5, + "22": 1.4545454545454546, + "24": 1.5, + "26": 1.3076923076923077, + "28": 1.4285714285714286, + "30": 1.3333333333333333, + "36": 1.3333333333333333, + "40": 0.9, + "60": 0.8333333333333334, + "80": 0.875, + "120": 0.8333333333333334 + }, + "tablet": { + "20": 1.6, + "60": 0.9333333333333333 + } + }, + "fontWeight": { + "regular": 400, + "medium": 500 + } + }, + "colors": { + "colorPalette": { + "black": { + "base": "#111111", + "light": "rgba(0, 0, 0, 0.75)", + "dark": "#0D0D0D" + }, + "white": { + "base": "#ffffff", + "light": "rgba(255, 255, 255, 0.75)", + "dark": "#BFBFBF" + }, + "grey": { + "base": "#757575", + "light": "rgba(117, 117, 117, 0.75)", + "dark": "#585858" + }, + "orange": { + "base": "#FA5400", + "light": "rgba(250, 84, 0, 0.75)", + "dark": "#BC3F00" + }, + "red": { + "base": "#d43f21" + }, + "green": { + "base": "#128a09" + } + }, + "brand": { + "black": "#111111", + "white": "#ffffff", + "grey": "#757575", + "orange": "#FA5400", + "red": "#d43f21", + "green": "#128a09", + "inactiveGrey": "#CCCCCC", + "borderGrey": "#E5E5E5", + "primaryGrey": "#F5F5F5", + "secondaryGrey": "#FAFAFA", + "accent": "#FA5400", + "error": "#d43f21", + "success": "#128a09" + } + }, + "breakpoints": { + "mobile": 0, + "tablet": 600, + "desktop": 960, + "largeDesktop": 1440, + "extraLargeDesktop": 1920 + }, + "mediaQueries": { + "mobile": "only screen and (min-width: 0px)", + "sm": "(max-width: 599px)", + "md": "(min-width: 600px) and (max-width: 959px)", + "lg": "(min-width: 960px) and (max-width: 1439px)", + "xl": "(min-width: 1440px) and (max-width: 1919px)", + "xxl": "(min-width: 1920px)", + "tablet": "only screen and (min-width: 600px)", + "desktop": "only screen and (min-width: 960px)", + "largeDesktop": "only screen and (min-width: 1440px)", + "extraLargeDesktop": "only screen and (min-width: 1920px)" + }, + "zIndex": { + "z0": 0, + "z1": 1, + "z2": 2, + "z3": 3, + "z4": 4, + "z5": 5, + "z6": 6, + "z7": 7, + "z8": 8, + "z9": 9, + "z10": 10, + "low": 0, + "mid": 5, + "high": 10 + }, + "scaleIncrement": 4, + "spacing": { + "baseline": 16, + "padding": "4px", + "scale": [ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 64, 68, + 72, 76, 80, 84, 88, 92, 96, 100, 104, 108, 112, 116 + ] + }, + "layout": { + "gutter": 16, + "maxWidth": 1808, + "grid": { + "columnCount": 12, + "columnWidth": { + "1": "8.333333333333334%", + "2": "16.666666666666668%", + "3": "25%", + "4": "33.333333333333336%", + "5": "41.66666666666667%", + "6": "50%", + "7": "58.333333333333336%", + "8": "66.66666666666667%", + "9": "75%", + "10": "83.33333333333334%", + "11": "91.66666666666667%", + "12": "100%" + } + } + }, + "transition": { + "default": { + "duration": "250ms", + "timing": "cubic-bezier(0.77, 0, 0.175, 1)", + "transition": "all 250ms cubic-bezier(0.77, 0, 0.175, 1)" + } + }, + "borderRadius": 24 + } + } +} +``` + +Colors: + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +### Internationalization + +If we examine the network requests, we can already identify some requests that include "i18n" in their path, such as on the favorites page (https://www.nike.com/favorites): + +***https://www.nike.com/assets/i18n/dotcom-fragments-recommendations/en-US.json*** + +Here, the response is a JSON object with a key containing an object with "value" and "description" keys: + +```json +{ + "favorites.productRecommendationsCarousel.title": { + "value": "You Might Also Like", + "description": "The title for the product recommendations carousel displayed on the favorites page" + }, + "profile.favoritesCarousel.title": { + "value": "Favorites", + "description": "Title for the favorites section" + }, + "profile.favoritesCarousel.viewAllLink": { + "value": "View All", + "description": "The link that takes users to the page where they can view all of their Favorites" + }, + "recommendations.badges.member_access_label": { + "value": "Member Access", + "description": "This text is displayed in carousel card for member exclusive product" + }, + "recommendations.badges.member_exclusive": { + "value": "Get this product with your free Nike Membership Profile", + "description": "This text is displayed in carousel card for member exclusive product" + }, + "recommendations.badges.nike_by_you": { + "value": "Nike by You", + "description": "This text is displayed in carousel card for Nike by You product" + }, + "recommendations.badges.nike_by_you_label": { + "value": "Customize", + "description": "This text is displayed in carousel card for Nike by You product" + }, + "recommendations.favorites.title": { + "value": "Find Your Next Favorite", + "description": "Default Title of Recommendations" + }, + "recommendations.next.label": { + "value": "Next Recommended Product", + "description": "Next Button Title" + }, + "recommendations.nullsearch.title": { + "value": "These popular items might interest you", + "description": "Product wall null search title for recommendations carousel" + }, + "recommendations.previous.label": { + "value": "Previous Recommended Product", + "description": "Previous Product Label for Button" + }, + "recommendations.top_trending": { + "value": "Top Trending", + "description": "Title for a component which will display our top trending products for user's market place" + } +} +``` + +We can also find translations within the script using the `