├── .DS_Store ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── TODO.md ├── assets ├── init.css └── ssense.css ├── components ├── core │ ├── Head.js │ ├── Image.js │ ├── Image.module.css │ ├── Layout.js │ ├── Layout.module.css │ ├── MainNav.js │ └── MainNav.module.css ├── examples │ ├── common │ │ ├── Banner.js │ │ ├── Feed.js │ │ ├── Footer.js │ │ ├── Header.js │ │ ├── Header.module.css │ │ ├── Item.js │ │ └── Stats.js │ ├── i18n │ │ └── locales │ │ │ ├── en.json │ │ │ └── es.json │ └── trr │ │ ├── Banner.js │ │ ├── Footer.js │ │ ├── Header.js │ │ └── Main.js └── real │ ├── ArticleWidget.js │ ├── ArticleWidget.module.css │ ├── ArticleWidgetRow.js │ ├── ArticleWidgetRow.module.css │ ├── BlogWidget.js │ ├── BlogWidget.module.css │ ├── CTA.js │ ├── CTA.module.css │ ├── Container.js │ ├── Container.module.css │ ├── Footer.js │ ├── Footer.module.css │ ├── Header.js │ ├── Header.module.css │ ├── MainContainer.js │ ├── MainContainer.module.css │ ├── MainReadWidget.js │ ├── MainReadWidget.module.css │ ├── ReadWidget.js │ ├── ReadWidget.module.css │ ├── ReadWidgetRow.js │ ├── ReadWidgetRow.module.css │ ├── ShopWidget.js │ └── ShopWidget.module.css ├── helpers └── uuid.js ├── jsconfig.json ├── lib ├── bc.js └── cart.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── concepts │ ├── isr.js │ ├── item │ │ └── [slug].js │ ├── ssg.js │ └── ssr.js ├── examples │ ├── common.js │ ├── real.js │ └── skeleton.js ├── index.js └── real │ └── store-0 │ └── index.js ├── postcss.config.json ├── prismic-configuration.js ├── public ├── favicon.ico └── favicon.png ├── tailwind.config.js └── vercel.json /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okbel/next-store/c53f6265280bde97c134a9273b3ab5a9742fbee1/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | .vercel 4 | .env 5 | .next -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.ignoreLimitWarning": true 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # next-store 2 | 3 | A proof of concept using a NextJS seed ready to be deployed with Vercel. 4 | 5 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/import/git?c=1&s=https://github.com/okbel/next-store) 6 | 7 | ### Features 8 | 9 | - Lazy Image Loading x2 10 | - CSS Purged 11 | - Green on Lighthouse 12 | 13 | ![Image of Lighthouse](https://res.cloudinary.com/vercel/image/upload/v1592422143/lighthouse_agjpn1.png) 14 | 15 | ### Tech Stack 16 | 17 | - Nextjs 18 | - PurgeCSS 19 | - TailwindCSS 20 | - PostCSS 21 | - Allows nesting and imports. 22 | 23 | ## Up and Running 24 | 25 | - Develop running `npm run dev` - [http://localhost:3000](http://localhost:3000) 26 | 27 | - Build `npm run build` 28 | 29 | - Deploy `vercel --prod` 30 | 31 | ## Demo 32 | 33 | [https://next-store-pi.vercel.app](https://next-store-pi.vercel.app) 34 | 35 | ## Data Fetching 36 | 37 | Using SWR: 38 | 39 | ```js 40 | import useSWR from "swr"; 41 | 42 | function Profile() { 43 | const { data, error } = useSWR("/api/user", fetcher); 44 | 45 | if (error) return
failed to load
; 46 | if (!data) return
loading...
; 47 | return
hello {data.name}!
; 48 | } 49 | ``` 50 | 51 | ## Examples and Resources 52 | 53 | - NextJS with Prismic [https://github.com/vercel/next.js/tree/canary/examples/cms-prismic](https://github.com/vercel/next.js/tree/canary/examples/cms-prismic) 54 | - Data fetching with [SWR](https://swr.now.sh/) 55 | - [Nextjs Documentation](https://nextjs.org/docs/getting-started) 56 | - Incremental Static Generation [https://static-tweet.now.sh/](https://static-tweet.now.sh/) 57 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | Clothing Real E-Commerce 4 | 5 | - Add Skeleton Loading to widgets 6 | - Add correct fonts 7 | -------------------------------------------------------------------------------- /assets/init.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --nc-font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 3 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif, 4 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 5 | --nc-font-mono: Consolas, monaco, "Ubuntu Mono", "Liberation Mono", 6 | "Courier New", Courier, monospace; 7 | --nc-tx-1: #000000; 8 | --nc-tx-2: #1a1a1a; 9 | --nc-bg-1: #ffffff; 10 | --nc-bg-2: #f6f8fa; 11 | --nc-bg-3: #e5e7eb; 12 | --nc-lk-1: #0070f3; 13 | --nc-lk-2: #0366d6; 14 | --nc-lk-tx: #ffffff; 15 | --nc-ac-1: #79ffe1; 16 | --nc-ac-tx: #0c4047; 17 | --accents-1: #fafafa; 18 | --accents-2: #eaeaea; 19 | } 20 | 21 | html, 22 | body { 23 | overflow-x: hidden; 24 | } 25 | 26 | body { 27 | margin: 0; 28 | -webkit-font-smoothing: antialiased; 29 | -moz-osx-font-smoothing: grayscale; 30 | } 31 | 32 | *, 33 | :after, 34 | :before { 35 | box-sizing: inherit; 36 | border: 0 solid #e2e8f0; 37 | } 38 | 39 | html { 40 | touch-action: manipulation; 41 | font-feature-settings: "case" 1, "rlig" 1, "calt" 0; 42 | text-rendering: optimizeLegibility; 43 | -webkit-font-smoothing: antialiased; 44 | -moz-osx-font-smoothing: grayscale; 45 | font-family: var(--nc-font-sans); 46 | line-height: 1.5; 47 | } 48 | 49 | @keyframes loading { 50 | 0% { 51 | background-position: 200% 0; 52 | } 53 | 54 | to { 55 | background-position: -200% 0; 56 | } 57 | } 58 | 59 | .skeleton { 60 | display: block; 61 | width: 100%; 62 | border-radius: 5px; 63 | background-image: linear-gradient( 64 | 270deg, 65 | var(--accents-1), 66 | var(--accents-2), 67 | var(--accents-2), 68 | var(--accents-1) 69 | ); 70 | background-size: 400% 100%; 71 | animation: loading 8s ease-in-out infinite; 72 | } 73 | 74 | @tailwind base; 75 | @tailwind components; 76 | @tailwind utilities; 77 | @tailwind screens; 78 | -------------------------------------------------------------------------------- /assets/ssense.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: plain; 3 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-regular-webfont.eot"); 4 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-regular-webfont.eot?#iefix") 5 | format("embedded-opentype"), 6 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-regular-webfont.woff2") 7 | format("woff2"), 8 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-regular-webfont.woff") 9 | format("woff"), 10 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-regular-webfont.ttf") 11 | format("truetype"), 12 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-regular-webfont.svg#plainregular") 13 | format("svg"); 14 | font-weight: 400; 15 | font-style: normal; 16 | } 17 | 18 | @font-face { 19 | font-family: plain; 20 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-medium-webfont.eot"); 21 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-medium-webfont.eot?#iefix") 22 | format("embedded-opentype"), 23 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-medium-webfont.woff2") 24 | format("woff2"), 25 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-medium-webfont.woff") 26 | format("woff"), 27 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-medium-webfont.ttf") 28 | format("truetype"), 29 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-medium-webfont.svg#plainmedium") 30 | format("svg"); 31 | font-weight: 500; 32 | font-style: normal; 33 | } 34 | 35 | @font-face { 36 | font-family: plain; 37 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-bold-webfont.eot"); 38 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-bold-webfont.eot?#iefix") 39 | format("embedded-opentype"), 40 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-bold-webfont.woff2") 41 | format("woff2"), 42 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-bold-webfont.woff") 43 | format("woff"), 44 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-bold-webfont.ttf") 45 | format("truetype"), 46 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1427392336/web/fonts/plain/plain-bold-webfont.svg#plainbold") 47 | format("svg"); 48 | font-weight: 700; 49 | font-style: normal; 50 | } 51 | 52 | @font-face { 53 | font-family: Favorit SSENSE Inter; 54 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.eot"); 55 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.eot?#iefix") 56 | format("embedded-opentype"), 57 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.woff2") 58 | format("woff2"), 59 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.woff") 60 | format("woff"); 61 | font-weight: 400; 62 | font-style: normal; 63 | } 64 | 65 | @font-face { 66 | font-family: Favorit SSENSE Inter1; 67 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.eot"); 68 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.eot?#iefix") 69 | format("embedded-opentype"), 70 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.woff2") 71 | format("woff2"), 72 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Inter.woff") 73 | format("woff"); 74 | font-weight: 400; 75 | font-style: normal; 76 | } 77 | 78 | @font-face { 79 | font-family: Favorit SSENSE Regular; 80 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Regular.eot"); 81 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Regular.eot?#iefix") 82 | format("embedded-opentype"), 83 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Regular.woff2") 84 | format("woff2"), 85 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1504879826/web/fonts/favorit-times/FavoritSSENSE-Regular.woff") 86 | format("woff"); 87 | font-weight: 400; 88 | font-style: normal; 89 | } 90 | 91 | @font-face { 92 | font-family: JHA Times Now; 93 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1493055756/web/fonts/favorit-times/TimesNow-SemiLight.eot"); 94 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1493055756/web/fonts/favorit-times/TimesNow-SemiLight.eot?#iefix") 95 | format("embedded-opentype"), 96 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1493055756/web/fonts/favorit-times/TimesNow-SemiLight.woff2") 97 | format("woff2"), 98 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1493055756/web/fonts/favorit-times/TimesNow-SemiLight.woff") 99 | format("woff"); 100 | font-weight: 400; 101 | font-style: normal; 102 | } 103 | 104 | @font-face { 105 | font-family: ssense-fonts; 106 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1492612618/web/fonts/ssense-font/ssense-fonts.eot"); 107 | src: url("//res.cloudinary.com/ssenseweb/raw/upload/v1492612618/web/fonts/ssense-font/ssense-fonts.eot") 108 | format("embedded-opentype"), 109 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1492612618/web/fonts/ssense-font/ssense-fonts.woff") 110 | format("woff"), 111 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1492612618/web/fonts/ssense-font/ssense-fonts.ttf") 112 | format("truetype"), 113 | url("//res.cloudinary.com/ssenseweb/raw/upload/v1471969601/web/fonts/ssense-font/ssense-fonts.svg") 114 | format("svg"); 115 | font-weight: 400; 116 | font-style: normal; 117 | } 118 | 119 | .fa, 120 | .heart-icon .button-label:before { 121 | font-family: ssense-fonts; 122 | speak: none; 123 | font-style: normal; 124 | font-weight: 400; 125 | font-variant: normal; 126 | text-transform: none; 127 | line-height: 1; 128 | display: inline-block; 129 | -webkit-font-smoothing: antialiased; 130 | -moz-osx-font-smoothing: grayscale; 131 | text-rendering: auto !important; 132 | } 133 | 134 | .fa-ssense-bag:before { 135 | content: "\E600"; 136 | } 137 | 138 | .fa-ssense-menu:before { 139 | content: "\E601"; 140 | } 141 | 142 | .fa-ssense-magnifier:before { 143 | content: "\E602"; 144 | } 145 | 146 | .fa-ssense-account:before { 147 | content: "\E603"; 148 | } 149 | 150 | .fa-twitter:before { 151 | content: "\E604"; 152 | } 153 | 154 | .fa-facebook:before { 155 | content: "\E605"; 156 | } 157 | 158 | .fa-instagram:before { 159 | content: "\E606"; 160 | } 161 | 162 | .fa-ssense-warning:before { 163 | content: "\E607"; 164 | } 165 | 166 | .fa-ssense-success:before { 167 | content: "\E608"; 168 | } 169 | 170 | .fa-soundcloud:before { 171 | content: "\E60A"; 172 | } 173 | 174 | .fa-ssense-printer:before { 175 | content: "\E60D"; 176 | } 177 | 178 | .fa-small-close:before { 179 | content: "\E611"; 180 | } 181 | 182 | .fa-listen-icon:before { 183 | content: "\E613"; 184 | } 185 | 186 | .fa-arrow-right:before { 187 | content: "\E615"; 188 | } 189 | 190 | .fa-arrow-left:before { 191 | content: "\E616"; 192 | } 193 | 194 | .fa-pinterest:before { 195 | content: "\E61A"; 196 | } 197 | 198 | .fa-google-plus:before { 199 | content: "\E61B"; 200 | } 201 | 202 | .fa-youtube:before { 203 | content: "\E61D"; 204 | } 205 | 206 | .fa-heartless:before { 207 | content: "\E900"; 208 | } 209 | 210 | .fa-heart:before { 211 | content: "\E901"; 212 | } 213 | -------------------------------------------------------------------------------- /components/core/Head.js: -------------------------------------------------------------------------------- 1 | import NextHead from "next/head"; 2 | 3 | const Head = () => ( 4 | 5 | next-store 6 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | 22 | export default Head; 23 | -------------------------------------------------------------------------------- /components/core/Image.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import useNativeLazyLoading from "@charlietango/use-native-lazy-loading"; 3 | import { useInView } from "react-intersection-observer"; 4 | import s from "./Image.module.css"; 5 | import cn from "classnames"; 6 | 7 | const CD_BUCKET = "v1592403664" || process.env.CD_BUCKET; 8 | const CD_CLOUD = "vercel" || process.env.CD_CLOUD; 9 | const CD_API = `https://res.cloudinary.com/${CD_CLOUD}/image/upload/`; 10 | 11 | export default ({ 12 | width, 13 | height, 14 | publicId, 15 | blur = 100, 16 | initialQuality = 10, 17 | className, 18 | ...rest 19 | }) => { 20 | const [loading, setLoading] = useState(true); 21 | const supportsLazyLoading = useNativeLazyLoading(); 22 | const [ref, inView] = useInView({ 23 | triggerOnce: true, 24 | rootMargin: "220px 0px", 25 | }); 26 | const ready = inView || supportsLazyLoading; 27 | 28 | useEffect(() => { 29 | // Setting loaded to true once, when it's appears in the viewport. 30 | if (ready) { 31 | setLoading(false); 32 | } 33 | }, [ready]); 34 | // The first render will try to load a bad quality image blurred. Then, when in view, it will load the best 35 | // quality and display it as soon is ready. 36 | const imgSrc = ready 37 | ? `${CD_API}q_100,${height ? `h_${height},` : ""}${ 38 | width ? `w_${width},` : "" 39 | }/${CD_BUCKET}/${publicId}` 40 | : `${CD_API}q_${initialQuality},${ 41 | height ? `h_${(height / 10).toFixed(0)},` : "" 42 | }${ 43 | width ? `w_${(width / 10).toFixed(0)},` : "" 44 | }/${CD_BUCKET}/${publicId}`; 45 | 46 | const pdB = (height / width) * 100; 47 | 48 | return ( 49 |
0 && width > 0 ? pdB.toFixed(1) : 100}%`, 54 | }} 55 | > 56 | 64 |
65 | ); 66 | }; 67 | -------------------------------------------------------------------------------- /components/core/Image.module.css: -------------------------------------------------------------------------------- 1 | @keyframes loading { 2 | 0% { 3 | background-position: 200% 0; 4 | } 5 | 6 | to { 7 | background-position: -200% 0; 8 | } 9 | } 10 | 11 | @keyframes appear { 12 | 0% { 13 | opacity: 0; 14 | } 15 | 100% { 16 | opacity: 1; 17 | } 18 | } 19 | 20 | .loading { 21 | img { 22 | filter: blur(15px); 23 | } 24 | } 25 | 26 | .imgContainer { 27 | @apply relative block overflow-hidden; 28 | display: block; 29 | width: 100%; 30 | /* border-radius: 5px; */ 31 | background-image: linear-gradient( 32 | 270deg, 33 | var(--accents-1), 34 | var(--accents-2), 35 | var(--accents-2), 36 | var(--accents-1) 37 | ); 38 | background-size: 400% 100%; 39 | animation: loading 8s ease-in-out infinite; 40 | } 41 | 42 | .img { 43 | position: absolute; 44 | width: 100%; 45 | height: 100%; 46 | animation: appear 0.25s ease-in; 47 | } 48 | -------------------------------------------------------------------------------- /components/core/Layout.js: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | import Link from "next/link"; 3 | import s from "./Layout.module.css"; 4 | 5 | export default function Layout({ children, showGoBack, styled }) { 6 | return ( 7 | <> 8 | 9 | 13 | 14 |
15 |
16 |

next-store

17 |

18 | A proof of concept using a NextJS seed ready to be 19 | deployed with Vercel. 20 |

21 | {showGoBack && ( 22 | 23 | Go back 24 | 25 | )} 26 |
27 |
{children}
28 |
29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/core/Layout.module.css: -------------------------------------------------------------------------------- 1 | .header { 2 | background: #f6f8fa; 3 | border-bottom: 1px solid #e5e7eb; 4 | padding: 2rem 1.5rem; 5 | margin: 0 calc(0px - (50vw - 50%)) 2rem; 6 | padding-left: calc(50vw - 50%); 7 | padding-right: calc(50vw - 50%); 8 | 9 | h1 { 10 | font-size: 2.25rem; 11 | } 12 | 13 | a { 14 | color: #0070f3; 15 | } 16 | a:hover { 17 | color: #0366d6; 18 | } 19 | } 20 | 21 | .styled { 22 | --nc-font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, 23 | Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif, 24 | "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 25 | --nc-font-mono: Consolas, monaco, "Ubuntu Mono", "Liberation Mono", 26 | "Courier New", Courier, monospace; 27 | --nc-tx-1: #000000; 28 | --nc-tx-2: #1a1a1a; 29 | --nc-bg-1: #ffffff; 30 | --nc-bg-2: #f6f8fa; 31 | --nc-bg-3: #e5e7eb; 32 | --nc-lk-1: #0070f3; 33 | --nc-lk-2: #0366d6; 34 | --nc-lk-tx: #ffffff; 35 | --nc-ac-1: #79ffe1; 36 | --nc-ac-tx: #0c4047; 37 | 38 | @media (prefers-color-scheme: dark) { 39 | body { 40 | --nc-tx-1: #ffffff; 41 | --nc-tx-2: #eeeeee; 42 | --nc-bg-1: #000000; 43 | --nc-bg-2: #111111; 44 | --nc-bg-3: #222222; 45 | --nc-lk-1: #3291ff; 46 | --nc-lk-2: #0070f3; 47 | --nc-lk-tx: #ffffff; 48 | --nc-ac-1: #7928ca; 49 | --nc-ac-tx: #ffffff; 50 | } 51 | } 52 | 53 | address, 54 | area, 55 | article, 56 | aside, 57 | audio, 58 | blockquote, 59 | datalist, 60 | details, 61 | dl, 62 | fieldset, 63 | figure, 64 | form, 65 | iframe, 66 | img, 67 | input, 68 | meter, 69 | nav, 70 | ol, 71 | optgroup, 72 | option, 73 | output, 74 | p, 75 | pre, 76 | progress, 77 | ruby, 78 | section, 79 | table, 80 | textarea, 81 | ul, 82 | video { 83 | margin-bottom: 1rem; 84 | } 85 | button, 86 | html, 87 | input, 88 | select { 89 | font-family: var(--nc-font-sans); 90 | } 91 | body { 92 | margin: 0 auto; 93 | max-width: 750px; 94 | padding: 2rem; 95 | border-radius: 6px; 96 | overflow-x: hidden; 97 | word-break: break-word; 98 | overflow-wrap: break-word; 99 | background: var(--nc-bg-1); 100 | color: var(--nc-tx-2); 101 | font-size: 1.03rem; 102 | line-height: 1.5; 103 | } 104 | ::selection { 105 | background: var(--nc-ac-1); 106 | color: var(--nc-ac-tx); 107 | } 108 | h1, 109 | h2, 110 | h3, 111 | h4, 112 | h5, 113 | h6 { 114 | line-height: 1; 115 | color: var(--nc-tx-1); 116 | padding-top: 0.875rem; 117 | } 118 | h1, 119 | h2, 120 | h3 { 121 | color: var(--nc-tx-1); 122 | padding-bottom: 2px; 123 | margin-bottom: 8px; 124 | border-bottom: 1px solid var(--nc-bg-2); 125 | } 126 | h4, 127 | h5, 128 | h6 { 129 | margin-bottom: 0.3rem; 130 | } 131 | h1 { 132 | font-size: 2.25rem; 133 | } 134 | h2 { 135 | font-size: 1.85rem; 136 | } 137 | h3 { 138 | font-size: 1.55rem; 139 | } 140 | h4 { 141 | font-size: 1.25rem; 142 | } 143 | h5 { 144 | font-size: 1rem; 145 | } 146 | h6 { 147 | font-size: 0.875rem; 148 | } 149 | a { 150 | color: var(--nc-lk-1); 151 | } 152 | a:hover { 153 | color: var(--nc-lk-2); 154 | } 155 | abbr:hover { 156 | cursor: help; 157 | } 158 | blockquote { 159 | padding: 1.5rem; 160 | background: var(--nc-bg-2); 161 | border-left: 5px solid var(--nc-bg-3); 162 | } 163 | abbr { 164 | cursor: help; 165 | } 166 | blockquote :last-child { 167 | padding-bottom: 0; 168 | margin-bottom: 0; 169 | } 170 | header { 171 | background: var(--nc-bg-2); 172 | border-bottom: 1px solid var(--nc-bg-3); 173 | padding: 2rem 1.5rem; 174 | margin: -2rem calc(0px - (50vw - 50%)) 2rem; 175 | padding-left: calc(50vw - 50%); 176 | padding-right: calc(50vw - 50%); 177 | } 178 | header h1, 179 | header h2, 180 | header h3 { 181 | padding-bottom: 0; 182 | border-bottom: 0; 183 | } 184 | header > :first-child { 185 | margin-top: 0; 186 | padding-top: 0; 187 | } 188 | header > :last-child { 189 | margin-bottom: 0; 190 | } 191 | a button, 192 | button, 193 | input[type="button"], 194 | input[type="reset"], 195 | input[type="submit"] { 196 | font-size: 1rem; 197 | display: inline-block; 198 | padding: 6px 12px; 199 | text-align: center; 200 | text-decoration: none; 201 | white-space: nowrap; 202 | background: var(--nc-lk-1); 203 | color: var(--nc-lk-tx); 204 | border: 0; 205 | border-radius: 4px; 206 | box-sizing: border-box; 207 | cursor: pointer; 208 | color: var(--nc-lk-tx); 209 | } 210 | a button[disabled], 211 | button[disabled], 212 | input[type="button"][disabled], 213 | input[type="reset"][disabled], 214 | input[type="submit"][disabled] { 215 | cursor: default; 216 | opacity: 0.5; 217 | cursor: not-allowed; 218 | } 219 | .button:focus, 220 | .button:hover, 221 | button:focus, 222 | button:hover, 223 | input[type="button"]:focus, 224 | input[type="button"]:hover, 225 | input[type="reset"]:focus, 226 | input[type="reset"]:hover, 227 | input[type="submit"]:focus, 228 | input[type="submit"]:hover { 229 | background: var(--nc-lk-2); 230 | } 231 | code, 232 | kbd, 233 | pre, 234 | samp { 235 | font-family: var(--nc-font-mono); 236 | } 237 | code, 238 | kbd, 239 | pre, 240 | samp { 241 | background: var(--nc-bg-2); 242 | border: 1px solid var(--nc-bg-3); 243 | border-radius: 4px; 244 | padding: 3px 6px; 245 | font-size: 0.9rem; 246 | } 247 | kbd { 248 | border-bottom: 3px solid var(--nc-bg-3); 249 | } 250 | pre { 251 | padding: 1rem 1.4rem; 252 | max-width: 100%; 253 | overflow: auto; 254 | } 255 | pre code { 256 | background: inherit; 257 | font-size: inherit; 258 | color: inherit; 259 | border: 0; 260 | padding: 0; 261 | margin: 0; 262 | } 263 | code pre { 264 | display: inline; 265 | background: inherit; 266 | font-size: inherit; 267 | color: inherit; 268 | border: 0; 269 | padding: 0; 270 | margin: 0; 271 | } 272 | details { 273 | padding: 0.6rem 1rem; 274 | background: var(--nc-bg-2); 275 | border: 1px solid var(--nc-bg-3); 276 | border-radius: 4px; 277 | } 278 | summary { 279 | cursor: pointer; 280 | font-weight: 700; 281 | } 282 | details[open] { 283 | padding-bottom: 0.75rem; 284 | } 285 | details[open] summary { 286 | margin-bottom: 6px; 287 | } 288 | details[open] > :last-child { 289 | margin-bottom: 0; 290 | } 291 | dt { 292 | font-weight: 700; 293 | } 294 | dd::before { 295 | content: "→ "; 296 | } 297 | hr { 298 | border: 0; 299 | border-bottom: 1px solid var(--nc-bg-3); 300 | margin: 1rem auto; 301 | } 302 | fieldset { 303 | margin-top: 1rem; 304 | padding: 2rem; 305 | border: 1px solid var(--nc-bg-3); 306 | border-radius: 4px; 307 | } 308 | legend { 309 | padding: auto 0.5rem; 310 | } 311 | table { 312 | border-collapse: collapse; 313 | width: 100%; 314 | } 315 | td, 316 | th { 317 | border: 1px solid var(--nc-bg-3); 318 | text-align: left; 319 | padding: 0.5rem; 320 | } 321 | th { 322 | background: var(--nc-bg-2); 323 | } 324 | tr:nth-child(even) { 325 | background: var(--nc-bg-2); 326 | } 327 | table caption { 328 | font-weight: 700; 329 | margin-bottom: 0.5rem; 330 | } 331 | textarea { 332 | max-width: 100%; 333 | } 334 | ol, 335 | ul { 336 | padding-left: 2rem; 337 | } 338 | li { 339 | margin-top: 0.4rem; 340 | } 341 | ol ol, 342 | ol ul, 343 | ul ol, 344 | ul ul { 345 | margin-bottom: 0; 346 | } 347 | mark { 348 | padding: 3px 6px; 349 | background: var(--nc-ac-1); 350 | color: var(--nc-ac-tx); 351 | } 352 | input, 353 | select, 354 | textarea { 355 | padding: 6px 12px; 356 | margin-bottom: 0.5rem; 357 | background: var(--nc-bg-2); 358 | color: var(--nc-tx-2); 359 | border: 1px solid var(--nc-bg-3); 360 | border-radius: 4px; 361 | box-shadow: none; 362 | box-sizing: border-box; 363 | } 364 | img { 365 | max-width: 100%; 366 | } 367 | ul { 368 | counter-reset: a; 369 | } 370 | 371 | li { 372 | padding-left: 25px; 373 | position: relative; 374 | padding-top: 0.5rem; 375 | } 376 | 377 | li:before { 378 | position: absolute; 379 | bottom: 2px; 380 | left: 0; 381 | font-size: 0.777777em; 382 | color: var(--nc-ac-tx); 383 | counter-increment: a; 384 | content: counter(a, decimal-leading-zero) "."; 385 | display: inline-block; 386 | width: 25px; 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /components/core/MainNav.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import s from "./MainNav.module.css"; 3 | 4 | const MainNav = () => ( 5 |
6 |
7 | Concepts: 8 |
9 |
10 | 11 | Static Generation (SSG) 12 | 13 |
14 |
15 | 16 | Server Rendered (SSR) 17 | 18 |
19 |
20 | 21 | Incremental Static Regeneration 22 | 23 |
24 |
25 | Examples: 26 |
27 |
28 | 29 | E-Commerce Common 30 | 31 |
32 |
33 | 34 | E-Commerce Skeleton 35 | 36 |
37 |
38 | 39 | E-Commerce Real Case Scenario 40 | 41 |
42 |
43 |
44 | Real Case Scenario: 45 |
46 |
47 | 48 | Clothing E-Commerce (WIP) 49 | 50 |
51 |
52 |
53 |
54 | 89 |
90 | ); 91 | 92 | export default MainNav; 93 | -------------------------------------------------------------------------------- /components/core/MainNav.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | a { 3 | color: white; 4 | } 5 | } 6 | 7 | .btn { 8 | @apply flex justify-center items-center box-border block mb-8 text-center rounded-lg bg-geist-blue px-3 py-3 text-white shadow-md; 9 | height: 50px; 10 | 11 | &:hover { 12 | @apply bg-white text-geist-blue border border-geist-blue transition ease-in; 13 | } 14 | } 15 | 16 | .row { 17 | @apply flex my-4 flex-col; 18 | 19 | @screen lg { 20 | @apply flex-row; 21 | } 22 | 23 | .col { 24 | @apply mb-3 h-12 w-full; 25 | 26 | @screen lg { 27 | @apply w-1/3; 28 | margin-right: 1rem; 29 | 30 | &:last-child { 31 | margin-right: 0; 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /components/examples/common/Banner.js: -------------------------------------------------------------------------------- 1 | export default function Banner() { 2 | return ( 3 |
4 |
5 |

6 | We announced a new product! 7 | 8 | Earn up to $500 in Site Credit!* Limited Time 9 | 10 | 11 | 12 | See Details → 13 | 14 | 15 |

16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/examples/common/Feed.js: -------------------------------------------------------------------------------- 1 | import Item from "./Item"; 2 | 3 | export default function Feed() { 4 | return ( 5 |
6 |
7 |
8 | 14 | 20 | 26 | 32 | 38 | 44 | 50 | 56 | 62 |
63 |
64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/examples/common/Footer.js: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |

10 | Solutions 11 |

12 | 46 |
47 |
48 |

49 | Support 50 |

51 | 69 |
70 |
71 |
72 |
73 |

74 | Company 75 |

76 | 102 |
103 |
104 |

105 | Legal 106 |

107 | 133 |
134 |
135 |
136 |
137 |

138 | Subscribe to our newsletter 139 |

140 |

141 | The latest news, articles, and resources, sent to your inbox 142 | weekly. 143 |

144 |
145 | 152 |
153 | 156 |
157 |
158 |
159 |
160 |
161 |
162 | 163 | Facebook 164 | 165 | 170 | 171 | 172 | 173 | Instagram 174 | 175 | 180 | 181 | 182 | 183 | Twitter 184 | 185 | 186 | 187 | 188 | 189 | GitHub 190 | 191 | 196 | 197 | 198 | 199 | Dribbble 200 | 201 | 206 | 207 | 208 |
209 |

210 | © 2020 MyStore, Inc. All rights reserved. 211 |

212 |
213 |
214 |
215 | ); 216 | } 217 | -------------------------------------------------------------------------------- /components/examples/common/Header.js: -------------------------------------------------------------------------------- 1 | import s from "./Header.module.css"; 2 | 3 | export default function Header() { 4 | return ( 5 |
6 |
7 |
8 |
9 | 10 | 16 | 21 | 22 | MyStore 23 | 24 |
25 |
26 | 44 |
45 | 65 |
66 | 70 | Sign in 71 | 72 | 73 | 77 | Sign up 78 | 79 | 80 |
81 |
82 |
83 |
84 |

25% OFF! in Iconic Handbags

85 | Shop LV, Hermès and more → 86 | 90 |
91 |
92 |
93 | Shop Beauty → 94 | 98 |
99 |
100 | Shop Home → 101 | 105 |
106 |
107 |
108 | ); 109 | } 110 | -------------------------------------------------------------------------------- /components/examples/common/Header.module.css: -------------------------------------------------------------------------------- 1 | .menuLink { 2 | color: red; 3 | @apply text-base leading-6 font-medium text-gray-700; 4 | 5 | &:hover { 6 | @apply text-gray-900; 7 | } 8 | 9 | &:focus { 10 | @apply text-gray-900 transition ease-in-out duration-150; 11 | } 12 | } 13 | 14 | .cta { 15 | @apply text-center uppercase text-white p-6 bg-black z-10 p-6 font-bold inline-block cursor-pointer; 16 | } 17 | 18 | .hero { 19 | @apply p-12 relative flex flex-col justify-center items-center overflow-hidden mt-6 rounded-md relative; 20 | min-height: 400px; 21 | 22 | h1 { 23 | @apply p-6 bg-black text-white font-medium z-10 text-4xl font-bold text-center; 24 | 25 | @screen md { 26 | @apply text-5xl; 27 | } 28 | } 29 | 30 | .cta { 31 | @apply mt-6; 32 | } 33 | } 34 | 35 | .heroImage { 36 | @apply w-full object-cover absolute z-0; 37 | height: 400px; 38 | } 39 | -------------------------------------------------------------------------------- /components/examples/common/Item.js: -------------------------------------------------------------------------------- 1 | import { MdFavorite, MdShoppingCart } from "react-icons/md"; 2 | 3 | function Item({ img = "", title = "", desc = "", price = "0" }) { 4 | return ( 5 |
6 |
7 | 8 | EDITORS PICK 9 | 10 | 11 |
12 |
13 |
14 |

15 | 16 | Jewelry 17 | 18 |

19 | 20 |

21 | {title} 22 |

23 |

{desc}

24 |
25 |
26 |
27 | ${price} 28 |
29 |
30 |
31 | 35 | 36 | Obsess 37 | 38 |
39 |
40 | 44 | 45 | Add to Cart 46 | Add 47 | 48 |
49 |
50 |
51 |
52 | ); 53 | } 54 | 55 | export default Item; 56 | -------------------------------------------------------------------------------- /components/examples/common/Stats.js: -------------------------------------------------------------------------------- 1 | function StatsItem({ upper = "", text = "" }) { 2 | return ( 3 |
4 |
5 |
6 |
7 | {upper} 8 |
9 |
10 | {text} 11 |
12 |
13 |
14 |
15 | ); 16 | } 17 | 18 | function Stats() { 19 | return ( 20 |
21 | 22 | 23 | 24 |
25 | ); 26 | } 27 | 28 | export default Stats; 29 | -------------------------------------------------------------------------------- /components/examples/i18n/locales/en.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/okbel/next-store/c53f6265280bde97c134a9273b3ab5a9742fbee1/components/examples/i18n/locales/en.json -------------------------------------------------------------------------------- /components/examples/i18n/locales/es.json: -------------------------------------------------------------------------------- 1 | { 2 | "routes": { 3 | "product": "productos" 4 | }, 5 | "content": { 6 | "title": "Tus Productos", 7 | "description": "Aquí puedes encontrar los mejores productos" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /components/examples/trr/Banner.js: -------------------------------------------------------------------------------- 1 | const Banner = () => ( 2 |
3 | 4 | 20% Off! Code: REAL — Terms Apply* 5 | 6 |
7 | ); 8 | 9 | export default Banner; 10 | -------------------------------------------------------------------------------- /components/examples/trr/Footer.js: -------------------------------------------------------------------------------- 1 | export default function Footer() { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |

11 | Solutions 12 |

13 | 47 |
48 |
49 |

50 | Support 51 |

52 | 70 |
71 |
72 |
73 |
74 |

75 | Company 76 |

77 | 103 |
104 |
105 |

106 | Legal 107 |

108 | 134 |
135 |
136 |
137 |
138 |

139 | Subscribe to our newsletter 140 |

141 |

142 | The latest news, articles, and resources, sent to your inbox 143 | weekly. 144 |

145 |
146 | 153 |
154 | 157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 | 167 | Facebook 168 | 169 | 174 | 175 | 176 | 177 | Instagram 178 | 179 | 184 | 185 | 186 | 187 | Twitter 188 | 189 | 190 | 191 | 192 | 193 | GitHub 194 | 195 | 200 | 201 | 202 | 203 | Dribbble 204 | 205 | 210 | 211 | 212 |
213 |
214 |
215 | A SUSTAINABLE LUXURY COMPANY 216 |
217 |

218 | Honoring heritage brands and extending the lifecycle of luxury items. 219 |

220 | The RealReal, Inc. 221 |
222 |
223 |

224 | All items are pre-owned and consigned to The RealReal. Trademarks are 225 | owned by their respective brand owners. No brand owner endorses or 226 | sponsors this ad or has any association and/or affiliation with The 227 | RealReal. 228 |

229 |
230 |
231 | ); 232 | } 233 | -------------------------------------------------------------------------------- /components/examples/trr/Header.js: -------------------------------------------------------------------------------- 1 | const Header = ({ menuLinks }) => ( 2 | <> 3 |
4 |
5 |
6 | Hi Belén, check your email for a $25 offer code. 7 | 8 | Updates for Our Customers and Community 9 | 10 |
11 |
12 | TheRealReal 13 |
14 |
MY TRR
15 |
16 | 34 |
35 |
36 | 37 | AUTHENTICATED LUXURY CONSIGNMENT 38 | 39 |
40 | 41 | ); 42 | 43 | export default Header; 44 | -------------------------------------------------------------------------------- /components/examples/trr/Main.js: -------------------------------------------------------------------------------- 1 | import Image from "../../../components/core/Image"; 2 | 3 | const Main = () => ( 4 |
5 |
6 | image 13 |
14 |
15 |
16 | image 23 |
24 | Iconic Handbags 25 | SHOP LV, HÈRMES, and MORE 26 |
27 |
28 |
29 |
30 | image 36 |
37 | Shoes with Sole 38 | 39 | Shop now 40 | 41 |
42 |
43 |
44 | image 50 |
51 | 52 | Shop now 53 | 54 |
55 |
56 |
57 |
58 |
59 |

Top Designers

60 |
61 |
62 |
63 | image 69 |
70 | 71 | GUCCI 72 | 73 |
74 |
75 |
76 | image 82 |
83 | 84 | Burberry 85 | 86 |
87 |
88 |
89 | image 95 |
96 | 97 | Prada 98 | 99 |
100 |
101 |
102 | image 108 |
109 | 110 | Louboutin 111 | 112 |
113 |
114 |
115 | image 121 |
122 | 123 | Louis Vuitton 124 | 125 |
126 |
127 |
128 | image 134 |
135 | 136 | HERMÈS 137 | 138 |
139 |
140 |
141 |
142 |

Top Categories

143 |
144 |
145 | image 151 |
152 |
153 | image 159 |
160 |
161 | image 167 |
168 |
169 |
170 |
171 | image 177 |
178 |
179 | *Promotional code will expire seven days from issue date. Code is valid on 180 | any item. Code may be split between multiple orders. Code does not apply 181 | to gift cards or shipping. Returned items originally purchased with the 182 | use of a code will result in the activation of the code, which will be 183 | valid for the following three days. 184 |
185 |
186 | ); 187 | 188 | export default Main; 189 | -------------------------------------------------------------------------------- /components/real/ArticleWidget.js: -------------------------------------------------------------------------------- 1 | import s from "./ArticleWidget.module.css"; 2 | import Image from "@/components/core/Image"; 3 | import cn from "classnames"; 4 | 5 | const ArticleWidget = ({ title, category, description, imgId }) => ( 6 |
7 |
8 | image 15 |
16 |
17 | {category} 18 |

{title}

19 |
20 |

{description}

21 |
22 | ); 23 | 24 | export default ArticleWidget; 25 | -------------------------------------------------------------------------------- /components/real/ArticleWidget.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply mb-6; 3 | 4 | @media (min-width: 768px) { 5 | @apply mr-6 flex-1 mb-2; 6 | max-width: 50%; 7 | 8 | &:last-child { 9 | @apply mr-0; 10 | } 11 | } 12 | } 13 | 14 | .category { 15 | @apply mr-8; 16 | 17 | color: rgb(51, 51, 51); 18 | font: lighter normal 0.9375rem/1.125rem JHA Times Now; 19 | letter-spacing: -0.00313rem; 20 | } 21 | 22 | .header { 23 | @apply flex items-center mt-4; 24 | } 25 | 26 | .title { 27 | @apply inline uppercase; 28 | color: rgb(51, 51, 51); 29 | margin: 0; 30 | font: lighter normal 1.125rem/1.5rem Favorit SSENSE Inter1; 31 | letter-spacing: -0.01562rem; 32 | } 33 | 34 | .description { 35 | @apply mt-1 mr-12; 36 | color: rgb(51, 51, 51); 37 | font: lighter normal 1.25rem/1.625rem JHA Times Now; 38 | letter-spacing: -0.00313rem; 39 | width: 100%; 40 | } 41 | 42 | .img { 43 | overflow: hidden; 44 | } 45 | 46 | .figure { 47 | margin: 0; 48 | } 49 | -------------------------------------------------------------------------------- /components/real/ArticleWidgetRow.js: -------------------------------------------------------------------------------- 1 | import s from "./ArticleWidgetRow.module.css"; 2 | import Image from "@/components/core/Image"; 3 | 4 | const ArticleWidgetRow = ({ title, category, description, imgId }) => ( 5 |
6 |
7 | image 14 |
15 |
16 |

{title}

17 | {category} 18 |

{description}

19 |
20 |
21 | ); 22 | 23 | export default ArticleWidgetRow; 24 | -------------------------------------------------------------------------------- /components/real/ArticleWidgetRow.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply flex flex-1 mb-8; 3 | } 4 | 5 | .category { 6 | @apply mr-8; 7 | color: rgb(51, 51, 51); 8 | font: lighter normal 0.9375rem/1.125rem JHA Times Now; 9 | letter-spacing: -0.00313rem; 10 | } 11 | 12 | .header { 13 | @apply flex flex-col; 14 | flex-basis: 45%; 15 | max-width: 400px; 16 | margin-left: 1.25rem; 17 | } 18 | 19 | .title { 20 | @apply inline uppercase; 21 | color: rgb(51, 51, 51); 22 | font: lighter normal 1.78125rem/2.125rem Favorit SSENSE Inter1; 23 | letter-spacing: -0.03125rem; 24 | } 25 | 26 | .description { 27 | @apply mr-12; 28 | color: rgb(51, 51, 51); 29 | font: lighter normal 1.25rem/1.625rem JHA Times Now; 30 | letter-spacing: -0.00313rem; 31 | width: 100%; 32 | margin-top: 1.25rem; 33 | } 34 | 35 | .img { 36 | background: gray; 37 | overflow: hidden; 38 | } 39 | 40 | .figure { 41 | margin: 0; 42 | flex-basis: 424px; 43 | min-width: 424px; 44 | } 45 | -------------------------------------------------------------------------------- /components/real/BlogWidget.js: -------------------------------------------------------------------------------- 1 | import s from "./BlogWidget.module.css"; 2 | import Image from "@/components/core/Image"; 3 | 4 | const BlogWidget = ({ 5 | title = "Don't forget your mask", 6 | date = "Aug 24", 7 | category = "Culture", 8 | imgId, 9 | }) => ( 10 |
11 |
12 | image 13 |
14 |
15 | Recent 16 |
17 |

{title}

18 |

19 | {category} | {date} 20 |

21 |
22 |
23 |
24 | ); 25 | 26 | export default BlogWidget; 27 | -------------------------------------------------------------------------------- /components/real/BlogWidget.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply mb-6; 3 | 4 | @media (min-width: 768px) { 5 | @apply flex-1 mr-6 mb-2; 6 | max-width: 33.33%; 7 | 8 | &:last-child { 9 | @apply mr-0; 10 | } 11 | } 12 | } 13 | 14 | .category { 15 | margin-top: 6px; 16 | color: rgb(51, 51, 51); 17 | letter-spacing: -0.00313rem; 18 | font-size: 16px; 19 | @apply mr-8 uppercase; 20 | } 21 | 22 | .header { 23 | @apply flex items-start mt-4; 24 | } 25 | 26 | .title { 27 | @apply inline uppercase; 28 | color: rgb(51, 51, 51); 29 | font: lighter normal 1.1875rem/1.625rem Favorit SSENSE Inter1; 30 | letter-spacing: -0.01562rem; 31 | font-weight: lighter; 32 | } 33 | 34 | .description { 35 | @apply mr-8; 36 | color: rgb(51, 51, 51); 37 | font: lighter normal 0.9375rem/1.125rem JHA Times Now; 38 | letter-spacing: -0.00313rem; 39 | } 40 | -------------------------------------------------------------------------------- /components/real/CTA.js: -------------------------------------------------------------------------------- 1 | import s from "./CTA.module.css"; 2 | 3 | const CTA = ({ title = "", description = "", text = "" }) => ( 4 |
5 |

{title}

6 |

{description}

7 | {text} 8 |
9 | ); 10 | 11 | export default CTA; 12 | -------------------------------------------------------------------------------- /components/real/CTA.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply text-center my-12; 3 | text-align: center; 4 | } 5 | 6 | .title { 7 | color: rgb(51, 51, 51); 8 | font-weight: 400; 9 | letter-spacing: 1px; 10 | margin: 0 auto 1.875rem; 11 | text-transform: uppercase; 12 | font-size: 5.8rem; 13 | max-width: 1180px; 14 | padding-left: 2.5rem; 15 | padding-right: 2.5rem; 16 | line-height: 5.5rem; 17 | 18 | font: lighter normal 5.9375rem/5.875rem Favorit SSENSE Inter1; 19 | letter-spacing: -0.28125rem; 20 | } 21 | 22 | .description { 23 | font-family: Times, "Times New Roman", serif; 24 | color: rgb(51, 51, 51); 25 | letter-spacing: -0.00313rem; 26 | font-weight: 100; 27 | max-width: 1180px; 28 | padding-left: 2.5rem; 29 | padding-right: 2.5rem; 30 | margin: 0 auto 1.875rem; 31 | font-size: 4rem; 32 | 33 | font: lighter normal 4.375rem/4.5rem JHA Times Now; 34 | letter-spacing: -0.175rem; 35 | } 36 | 37 | .btn { 38 | width: 160px; 39 | height: 40px; 40 | line-height: 36px; 41 | text-align: center; 42 | display: inline-block; 43 | border: 1px solid rgb(151, 151, 151); 44 | border-radius: 6px; 45 | color: rgb(51, 51, 51); 46 | font-weight: 300; 47 | letter-spacing: 0.5px; 48 | font-size: 14px; 49 | text-transform: uppercase; 50 | } 51 | -------------------------------------------------------------------------------- /components/real/Container.js: -------------------------------------------------------------------------------- 1 | import s from "./Container.module.css"; 2 | 3 | const Container = ({ narrow = false }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default Container; 8 | -------------------------------------------------------------------------------- /components/real/Container.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | padding: 0 0.625rem; 3 | 4 | @media (min-width: 768px) { 5 | padding: 0 1.25rem; 6 | } 7 | 8 | @media (min-width: 1024px) { 9 | padding: 0 1.875rem; 10 | } 11 | 12 | @media (min-width: 1440px) { 13 | padding: 0 2.5rem; 14 | } 15 | } 16 | 17 | .narrow { 18 | padding: 0 0.625rem; 19 | 20 | @media (min-width: 768px) { 21 | padding: 0 3.125rem; 22 | } 23 | 24 | @media (min-width: 1024px) { 25 | padding: 0 3.75rem; 26 | } 27 | 28 | @media (min-width: 1440px) { 29 | padding: 0 7.5rem; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /components/real/Footer.js: -------------------------------------------------------------------------------- 1 | import s from "./Footer.module.css"; 2 | 3 | const Footer = () => { 4 | return ( 5 | 27 | ); 28 | }; 29 | 30 | export default Footer; 31 | -------------------------------------------------------------------------------- /components/real/Footer.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply px-10 mb-6; 3 | 4 | .container { 5 | @apply flex justify-between mx-auto; 6 | max-width: 1680px; 7 | } 8 | 9 | > nav { 10 | @apply items-center; 11 | } 12 | 13 | a, 14 | span { 15 | margin: 0 12px; 16 | display: inline-block; 17 | white-space: nowrap; 18 | font-family: Favorit SSENSE Inter; 19 | line-height: 15px; 20 | font-size: 11px; 21 | } 22 | 23 | a:hover { 24 | cursor: pointer; 25 | background-image: linear-gradient( 26 | 180deg, 27 | transparent 50%, 28 | rgba(0, 0, 0, 0.75) 0 29 | ); 30 | background-repeat: repeat-x; 31 | background-size: 2px 2px; 32 | background-position: 0 1.1em; 33 | } 34 | } 35 | 36 | .sideNav { 37 | @apply flex-grow-0 mb-0; 38 | color: #888; 39 | } 40 | -------------------------------------------------------------------------------- /components/real/Header.js: -------------------------------------------------------------------------------- 1 | import s from "./Header.module.css"; 2 | 3 | const Header = () => ( 4 |
5 |
6 | 10 |

SSENSE

11 | 15 |
16 |
17 | 22 |

SSENSE

23 | 29 |
30 |
31 | ); 32 | 33 | export default Header; 34 | -------------------------------------------------------------------------------- /components/real/Header.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | .menu, 3 | .mobileMenu { 4 | font-family: Favorit SSENSE Inter; 5 | @apply flex justify-between items-center relative; 6 | position: fixed; 7 | background-color: white; 8 | width: 100%; 9 | top: 0; 10 | left: 0; 11 | height: 55px; 12 | line-height: 55px; 13 | padding: 0 16px; 14 | z-index: 10; 15 | } 16 | 17 | .mobileMenu nav > * { 18 | padding: 8px; 19 | font-size: 1.1rem; 20 | box-sizing: border-box; 21 | background-image: none; 22 | background-repeat: none; 23 | background-size: 0; 24 | background-position: 0; 25 | text-decoration: none; 26 | } 27 | 28 | .menu { 29 | display: none; 30 | padding-left: 55px; 31 | padding-right: 55px; 32 | } 33 | 34 | @media (min-width: 1040px) { 35 | .mobileMenu { 36 | display: none; 37 | } 38 | 39 | .menu { 40 | display: flex; 41 | } 42 | } 43 | } 44 | 45 | .logo { 46 | @apply text-2xl absolute; 47 | left: 50%; 48 | transform: translateX(-50%); 49 | font-weight: 600; 50 | letter-spacing: 1px; 51 | } 52 | 53 | .nav { 54 | @apply flex justify-between items-center; 55 | } 56 | 57 | .link { 58 | @apply mr-6 uppercase text-xs; 59 | font-weight: 500; 60 | white-space: nowrap; 61 | cursor: pointer; 62 | font-size: 11px; 63 | &:last-child { 64 | margin-right: 0; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /components/real/MainContainer.js: -------------------------------------------------------------------------------- 1 | import s from "./MainContainer.module.css"; 2 | 3 | const MainContainer = ({ children }) => ( 4 |
{children}
5 | ); 6 | 7 | export default MainContainer; 8 | -------------------------------------------------------------------------------- /components/real/MainContainer.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply mx-auto pt-1 p-2 px-0; 3 | max-width: 1680px; 4 | 5 | font-family: Favorit SSENSE Inter; 6 | font-weight: 400; 7 | color: #000; 8 | font-size: 11px; 9 | line-height: 15px; 10 | background-color: #fff; 11 | text-rendering: optimizeLegibility; 12 | 13 | > section { 14 | @apply flex content-between justify-center my-16 flex-col mx-auto; 15 | padding: 0 0.625rem; 16 | 17 | @media (min-width: 768px) { 18 | @apply flex-row; 19 | padding: 0 1.25rem; 20 | } 21 | 22 | @media (min-width: 1024px) { 23 | padding: 0 1.875rem; 24 | } 25 | 26 | @media (min-width: 1440px) { 27 | padding: 0 2.5rem; 28 | } 29 | } 30 | 31 | > section:nth-child(1), 32 | > section:nth-child(3), 33 | > section:nth-child(5), 34 | > section:nth-child(7), 35 | > section:nth-child(8) { 36 | padding: 0 0.625rem; 37 | 38 | @media (min-width: 768px) { 39 | padding: 0 3.125rem; 40 | } 41 | 42 | @media (min-width: 1024px) { 43 | padding: 0 3.75rem; 44 | } 45 | 46 | @media (min-width: 1440px) { 47 | padding: 0 7.5rem; 48 | } 49 | } 50 | 51 | > section:nth-child(1) { 52 | @apply mt-16; 53 | } 54 | 55 | > section:nth-child(6) { 56 | > div { 57 | flex-basis: calc((((100vw - 30px) / 5) * 1) - 30px); 58 | margin: 0 20px; 59 | } 60 | 61 | > div:first-child { 62 | @apply flex flex-1 flex-col items-start; 63 | flex-basis: calc((((100vw - 30px) / 5) * 3) - 15px); 64 | } 65 | 66 | > div:last-child { 67 | @apply flex flex-col; 68 | flexbasis: calc((((100vw - 30px) / 5) * 1) - 30px); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /components/real/MainReadWidget.js: -------------------------------------------------------------------------------- 1 | import s from "./MainReadWidget.module.css"; 2 | 3 | const BlogWidget = ({ headline = "A GOOD READ" }) => ( 4 |
5 |

{headline}

6 | View All Stories 7 |
8 | ); 9 | 10 | export default BlogWidget; 11 | -------------------------------------------------------------------------------- /components/real/MainReadWidget.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | display: none; 3 | border-top: 1px solid #979797; 4 | border-bottom: 1px solid #979797; 5 | padding: 0.625rem 0; 6 | max-width: calc((100vw - 20px) / 4); 7 | min-height: 149px; 8 | 9 | &:last-child { 10 | margin-right: 0; 11 | } 12 | 13 | @media (min-width: 1024px) { 14 | @apply flex mr-6 flex-col justify-between; 15 | } 16 | } 17 | 18 | .title { 19 | font: lighter normal 2.375rem/2.75rem Favorit SSENSE Inter1; 20 | text-transform: uppercase; 21 | letter-spacing: -0.07812rem; 22 | color: rgb(51, 51, 51); 23 | } 24 | 25 | .link { 26 | @apply uppercase; 27 | line-height: 18px; 28 | font-weight: 300; 29 | letter-spacing: 1px; 30 | font-size: 14px; 31 | color: rgb(51, 51, 51); 32 | } 33 | -------------------------------------------------------------------------------- /components/real/ReadWidget.js: -------------------------------------------------------------------------------- 1 | import s from "./ReadWidget.module.css"; 2 | import Image from "@/components/core/Image"; 3 | 4 | const BlogWidget = ({ title = "", category = "", imgId = "" }) => ( 5 |
6 |
7 | image 14 |
15 |
16 | {title} 17 |

{category}

18 |
19 |
20 | ); 21 | 22 | export default BlogWidget; 23 | -------------------------------------------------------------------------------- /components/real/ReadWidget.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply flex; 3 | border-top: 1px solid #979797; 4 | padding: 0.625rem 0; 5 | width: 100%; 6 | 7 | &:last-child { 8 | border-bottom: 1px solid #979797; 9 | } 10 | } 11 | 12 | .side { 13 | @apply mr-2; 14 | min-width: 95px; 15 | } 16 | 17 | .img { 18 | background: gray; 19 | } 20 | 21 | .title { 22 | @apply uppercase; 23 | color: rgb(51, 51, 51); 24 | font: lighter normal 0.89062rem/1.125rem Favorit SSENSE Inter1; 25 | letter-spacing: -0.00313rem; 26 | } 27 | 28 | .header { 29 | @apply flex flex-col justify-between; 30 | } 31 | 32 | .category { 33 | color: rgb(51, 51, 51); 34 | font: lighter normal 0.9375rem/1.125rem JHA Times Now; 35 | letter-spacing: -0.00313rem; 36 | } 37 | -------------------------------------------------------------------------------- /components/real/ReadWidgetRow.js: -------------------------------------------------------------------------------- 1 | import s from "./ReadWidgetRow.module.css"; 2 | import Image from "@/components/core/Image"; 3 | 4 | const BlogWidget = ({ title = "", category = "", imgId = "" }) => ( 5 |
6 |
7 | image 14 |
15 |
16 | {title} 17 |

{category}

18 |
19 |
20 | ); 21 | 22 | export default BlogWidget; 23 | -------------------------------------------------------------------------------- /components/real/ReadWidgetRow.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply flex; 3 | box-sizing: border-box; 4 | border-top: 1px solid #979797; 5 | border-bottom: 1px solid #979797; 6 | padding: 0.625rem 0; 7 | margin: 0 0.625rem; 8 | min-height: 106px; 9 | flex: 1; 10 | 11 | &:nth-child(3), 12 | &:nth-child(4) { 13 | display: none; 14 | } 15 | 16 | @media (min-width: 1040px) { 17 | @apply mr-6; 18 | width: calc((100vw - 20px) / 4); 19 | max-width: calc((100vw - 20px) / 4); 20 | min-height: 149px; 21 | 22 | &:nth-child(3), 23 | &:nth-child(4) { 24 | display: flex; 25 | } 26 | 27 | &:last-child { 28 | margin-right: 0; 29 | } 30 | } 31 | } 32 | 33 | .side { 34 | @apply mr-2; 35 | min-width: 33.3333%; 36 | } 37 | 38 | .img { 39 | background: gray; 40 | } 41 | 42 | .title { 43 | color: rgb(51, 51, 51); 44 | font: lighter normal 0.89062rem/1.125rem Favorit SSENSE Inter1; 45 | letter-spacing: -0.00313rem; 46 | text-align: center; 47 | text-transform: uppercase; 48 | margin-left: 0.625rem; 49 | } 50 | 51 | .header { 52 | @apply flex flex-col justify-between; 53 | } 54 | 55 | .category { 56 | color: rgb(51, 51, 51); 57 | font: lighter normal 0.9375rem/1.125rem JHA Times Now; 58 | letter-spacing: -0.00313rem; 59 | } 60 | -------------------------------------------------------------------------------- /components/real/ShopWidget.js: -------------------------------------------------------------------------------- 1 | import s from "./ShopWidget.module.css"; 2 | import Image from "@/components/core/Image"; 3 | 4 | const ShopWidget = ({ title = "", text = "", imgId = "" }) => ( 5 |
6 |
7 | image 14 |
15 |
16 | Featured 17 |

{title}

18 |
19 | 20 |
21 | ); 22 | 23 | export default ShopWidget; 24 | -------------------------------------------------------------------------------- /components/real/ShopWidget.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | @apply mb-4; 3 | 4 | @media (min-width: 768px) { 5 | @apply mr-6 flex-1; 6 | 7 | &:last-child { 8 | @apply mr-0; 9 | } 10 | } 11 | } 12 | 13 | .category { 14 | @apply mr-8; 15 | color: rgb(51, 51, 51); 16 | font: lighter normal 0.9375rem/1.125rem JHA Times Now; 17 | letter-spacing: -0.00313rem; 18 | } 19 | 20 | .header { 21 | @apply flex items-center mt-4 mb-2; 22 | } 23 | 24 | .title { 25 | @apply inline uppercase; 26 | color: rgb(51, 51, 51); 27 | font: lighter normal 1.1875rem/1.625rem Favorit SSENSE Inter1; 28 | letter-spacing: -0.01562rem; 29 | } 30 | 31 | .btn { 32 | width: 160px; 33 | height: 40px; 34 | line-height: 36px; 35 | text-align: center; 36 | display: block; 37 | border: 1px solid rgb(151, 151, 151); 38 | border-radius: 6px; 39 | color: rgb(51, 51, 51); 40 | 41 | font-weight: 300; 42 | letter-spacing: 0.5px; 43 | font-size: 14px; 44 | text-transform: uppercase; 45 | } 46 | 47 | .img { 48 | background: gray; 49 | overflow: hidden; 50 | } 51 | 52 | .figure { 53 | margin: 0; 54 | } 55 | -------------------------------------------------------------------------------- /helpers/uuid.js: -------------------------------------------------------------------------------- 1 | import { v4, v3 } from "uuid"; 2 | 3 | export function generateRandom() { 4 | const opts = { 5 | random: [ 6 | 0x10, 7 | 0x91, 8 | 0x56, 9 | 0xbe, 10 | 0xc4, 11 | 0xfb, 12 | 0xc1, 13 | 0xea, 14 | 0x71, 15 | 0xb4, 16 | 0xef, 17 | 0xe1, 18 | 0x67, 19 | 0x1c, 20 | 0x58, 21 | 0x36, 22 | ], 23 | }; 24 | return v4(); 25 | } 26 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/ui/*": ["components/ui/*"], 6 | "@/lib/*": ["lib/*"], 7 | "@/core/*": ["components/core/*"], 8 | "@/assets/*": ["assets/*"], 9 | "@/components/*": ["components/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/bc.js: -------------------------------------------------------------------------------- 1 | import "isomorphic-unfetch"; 2 | 3 | const API_URL = process.env.NEXT_EXAMPLE_BIGCOMMERCE_STOREFRONT_API_URL; 4 | const API_TOKEN = process.env.NEXT_EXAMPLE_BIGCOMMERCE_STOREFRONT_API_TOKEN; 5 | 6 | const responsiveImageFragment = ` 7 | fragment responsiveImageFragment on Image { 8 | url320wide: url(width: 320) 9 | url640wide: url(width: 640) 10 | url960wide: url(width: 960) 11 | url1280wide: url(width: 1280) 12 | } 13 | `; 14 | 15 | async function fetchAPI(query, { variables, preview } = {}) { 16 | const res = await fetch(API_URL + (preview ? "/preview" : ""), { 17 | method: "POST", 18 | headers: { 19 | "Content-Type": "application/json", 20 | Authorization: `Bearer ${API_TOKEN}`, 21 | }, 22 | body: JSON.stringify({ 23 | query, 24 | variables, 25 | }), 26 | }); 27 | 28 | const json = await res.json(); 29 | if (json.errors) { 30 | console.error(json.errors); 31 | throw new Error("Failed to fetch API"); 32 | } 33 | return json.data; 34 | } 35 | 36 | export async function getPreviewPostBySlug(slug) { 37 | const data = await fetchAPI( 38 | ` 39 | query PostBySlug($slug: String) { 40 | post(filter: {slug: {eq: $slug}}) { 41 | slug 42 | } 43 | }`, 44 | { 45 | preview: true, 46 | variables: { 47 | slug, 48 | }, 49 | } 50 | ); 51 | return data?.post; 52 | } 53 | 54 | export async function getAllPostsWithSlug() { 55 | const data = fetchAPI(` 56 | { 57 | allPosts { 58 | slug 59 | } 60 | } 61 | `); 62 | return data?.allPosts; 63 | } 64 | 65 | export async function getAllProductsForHome() { 66 | const data = await fetchAPI( 67 | ` 68 | query paginateProducts { 69 | site { 70 | products (first: 4) { 71 | pageInfo { 72 | startCursor 73 | endCursor 74 | } 75 | edges { 76 | cursor 77 | node { 78 | entityId 79 | name 80 | path 81 | brand { 82 | name 83 | } 84 | description 85 | prices { 86 | price { 87 | value 88 | currencyCode 89 | } 90 | salePrice { 91 | value 92 | currencyCode 93 | } 94 | } 95 | images { 96 | edges { 97 | node { 98 | ...responsiveImageFragment 99 | } 100 | } 101 | } 102 | variants { 103 | edges { 104 | node { 105 | entityId 106 | defaultImage { 107 | ...responsiveImageFragment 108 | } 109 | } 110 | } 111 | } 112 | options { 113 | edges { 114 | node { 115 | entityId 116 | displayName 117 | isRequired 118 | values { 119 | edges { 120 | node { 121 | entityId 122 | label 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | } 132 | } 133 | } 134 | 135 | ${responsiveImageFragment} 136 | ` 137 | ); 138 | return data?.site?.products?.edges; 139 | } 140 | 141 | export async function getAllProductsWithSlug() { 142 | const data = fetchAPI(` 143 | { 144 | site { 145 | products { 146 | edges { 147 | node { 148 | path 149 | } 150 | } 151 | } 152 | } 153 | } 154 | `); 155 | return data?.site?.products?.edges; 156 | } 157 | 158 | export async function getProduct(slug) { 159 | const data = await fetchAPI( 160 | ` 161 | query ProductBySlug($slug: String!) { 162 | site { 163 | route(path: $slug) { 164 | node { 165 | __typename 166 | ... on Product { 167 | entityId 168 | name 169 | path 170 | brand { 171 | name 172 | } 173 | description 174 | prices { 175 | price { 176 | currencyCode 177 | value 178 | } 179 | salePrice { 180 | currencyCode 181 | value 182 | } 183 | } 184 | images { 185 | edges { 186 | node { 187 | url640wide: url(width: 640) 188 | url1280wide: url(width: 1280) 189 | } 190 | } 191 | } 192 | variants { 193 | edges { 194 | node { 195 | entityId 196 | } 197 | } 198 | } 199 | options { 200 | edges { 201 | node { 202 | entityId 203 | displayName 204 | isRequired 205 | values { 206 | edges { 207 | node { 208 | entityId 209 | label 210 | } 211 | } 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | } 220 | } 221 | `, 222 | { 223 | variables: { 224 | slug, 225 | }, 226 | } 227 | ); 228 | return data; 229 | } 230 | 231 | export async function getRelatedProducts(slug) { 232 | const data = await fetchAPI( 233 | ` 234 | { 235 | site { 236 | products(entityIds: [80,81,82,83]) { 237 | edges { 238 | node { 239 | entityId 240 | name 241 | path 242 | brand { 243 | name 244 | } 245 | description 246 | prices { 247 | price { 248 | currencyCode 249 | value 250 | } 251 | salePrice { 252 | currencyCode 253 | value 254 | } 255 | } 256 | images { 257 | edges { 258 | node { 259 | url640wide: url(width: 640) 260 | url1280wide: url(width: 1280) 261 | } 262 | } 263 | } 264 | variants { 265 | edges { 266 | node { 267 | entityId 268 | } 269 | } 270 | } 271 | } 272 | } 273 | } 274 | } 275 | } 276 | `, 277 | { 278 | variables: { 279 | slug, 280 | }, 281 | } 282 | ); 283 | 284 | return data; 285 | } 286 | -------------------------------------------------------------------------------- /lib/cart.js: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from "react"; 2 | import useSWR, { mutate } from "swr"; 3 | 4 | async function getText(res) { 5 | try { 6 | return (await res.text()) || res.statusText; 7 | } catch (error) { 8 | return res.statusText; 9 | } 10 | } 11 | 12 | async function getError(res) { 13 | if (res.headers.get("Content-Type")?.includes("application/json")) { 14 | const data = await res.json(); 15 | return data.errors[0]; 16 | } 17 | return { message: await getText(res) }; 18 | } 19 | 20 | async function fetcher(url) { 21 | const res = await fetch(url); 22 | 23 | if (res.status === 200) { 24 | return res.json(); 25 | } 26 | throw await getError(res); 27 | } 28 | 29 | export function useCart() { 30 | return useSWR("/api/cart", fetcher); 31 | } 32 | 33 | export function useAddToCart() { 34 | const [{ addingToCart, error }, setStatus] = useState({ 35 | addingToCart: false, 36 | }); 37 | const addToCart = useCallback(async ({ product }) => { 38 | setStatus({ addingToCart: true }); 39 | 40 | const res = await fetch("/api/cart", { 41 | method: "POST", 42 | headers: { 43 | "Content-Type": "application/json", 44 | }, 45 | body: JSON.stringify({ product }), 46 | }); 47 | 48 | // Product added as expected 49 | if (res.status === 200) { 50 | setStatus({ addingToCart: false }); 51 | return mutate("/api/cart"); 52 | } 53 | 54 | const error = await getError(res); 55 | 56 | console.error("Adding product to cart failed with:", res.status, error); 57 | setStatus({ addingToCart: false, error }); 58 | }, []); 59 | 60 | return { addToCart, addingToCart, error }; 61 | } 62 | 63 | export function useUpdateCart() { 64 | const [{ updatingCart, error }, setStatus] = useState({ 65 | updatingCart: false, 66 | }); 67 | const updateCart = useCallback(async ({ product, item }) => { 68 | setStatus({ updatingCart: true }); 69 | 70 | const res = await fetch( 71 | `/api/cart?itemId=${item.id}`, 72 | product.quantity < 1 73 | ? { method: "DELETE" } 74 | : { 75 | method: "PUT", 76 | headers: { 77 | "Content-Type": "application/json", 78 | }, 79 | body: JSON.stringify({ product }), 80 | } 81 | ); 82 | 83 | // Product updated as expected 84 | if (res.status === 200) { 85 | setStatus({ updatingCart: false }); 86 | return mutate("/api/cart"); 87 | } 88 | 89 | const error = await getError(res); 90 | 91 | console.error("Update to cart failed with:", res.status, error); 92 | setStatus({ updatingCart: false, error }); 93 | }, []); 94 | 95 | return { updateCart, updatingCart, error }; 96 | } 97 | 98 | export function useRemoveFromCart() { 99 | const { updateCart, updatingCart, error } = useUpdateCart(); 100 | const removeFromCart = async ({ item }) => { 101 | updateCart({ item, product: { quantity: 0 } }); 102 | }; 103 | 104 | return { removeFromCart, removingFromCart: updatingCart, error }; 105 | } 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-store", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "dependencies": { 7 | "@charlietango/use-native-lazy-loading": "^1.7.1", 8 | "cloudinary-react": "^1.6.6", 9 | "contentful": "^7.14.6", 10 | "intersection-observer": "^0.10.0", 11 | "next": "^9.4.4", 12 | "postcss-exclude-files": "^1.0.15", 13 | "postcss-nested": "^4.2.1", 14 | "prismic-javascript": "^3.0.0", 15 | "prismic-reactjs": "^1.3.1", 16 | "react": "^16.13.1", 17 | "react-dom": "^16.13.1", 18 | "react-icons": "^3.11.0", 19 | "react-intersection-observer": "^8.26.2", 20 | "swr": "^0.2.3", 21 | "tailwindcss": "^1.4.6", 22 | "uuid": "^8.1.0" 23 | }, 24 | "devDependencies": { 25 | "postcss-loader": "^3.0.0" 26 | }, 27 | "scripts": { 28 | "dev": "next", 29 | "build": "next build", 30 | "start": "next start" 31 | }, 32 | "author": "Belen Curcio ", 33 | "license": "ISC" 34 | } 35 | -------------------------------------------------------------------------------- /pages/_app.js: -------------------------------------------------------------------------------- 1 | import "@/assets/init.css"; 2 | import "@/assets/ssense.css"; 3 | import Head from "../components/core/Head"; 4 | 5 | export default function MyApp({ Component, pageProps }) { 6 | return ( 7 | <> 8 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document_, { Head, Main, NextScript } from "next/document"; 2 | 3 | export default class Document extends Document_ { 4 | render() { 5 | return ( 6 | 7 | 8 | 9 |
10 | 11 | {/** 12 | * Here add your GA and more scripts. 13 | */} 14 | 15 | 16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pages/concepts/isr.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { useState } from "react"; 3 | import Layout from "../../components/core/Layout"; 4 | import { v4 as uuidv4 } from "uuid"; 5 | 6 | export default function Isg() { 7 | const [rand, setRand] = useState(uuidv4()); 8 | 9 | const changeRand = async () => { 10 | const newRand = uuidv4(); 11 | setRand(newRand); 12 | }; 13 | 14 | return ( 15 | 16 |

Incremental Static Regeneration

17 |

18 | Big and complex systems with many web pages encounter the issue of 19 | having to re-build an entire app for each deploy. Systems like this 20 | usually deal with this issue by using{" "} 21 | server side rendering and a caching layer (e.g Varnish) 22 |

23 |

24 | Rendering and building a page by request is not a solution. 25 | Neither is building the entire app everything at once. That's why we 26 | came up with Incremental Static Generation. 27 |

28 | This is useful for: 29 |
    30 |
  • Big applications, with loads of pages
  • 31 |
  • Architectures that tend to rely on SSR
  • 32 |
  • Dynamic Content, content changes fast
  • 33 |
34 |
35 |

Benefits:

36 |
    37 |
  • Works in the background, users always get a static response
  • 38 |
  • 39 | Pages get statically regenerated, including all changes from the CMS 40 |
  • 41 |
  • It's fully automatic, no need to trigger a full rebuild
  • 42 |
43 |
44 | 45 | 46 | 47 | 48 | Click here to change the random id. 49 | 50 |
51 |

Read more about this topic:

52 | 67 |
68 |

Up Next

69 |

Features that we are planning

70 |
    71 |
  • 72 | Re-generating and invalidating multiple pages at once (like your blog 73 | index and a certain blog post) 74 |
  • 75 |
  • 76 | Re-generating by listening to events (like CMS webhooks), ahead of 77 | user traffic 78 |
  • 79 |
80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /pages/concepts/item/[slug].js: -------------------------------------------------------------------------------- 1 | import Layout from "../../../components/core/Layout"; 2 | 3 | export default function IncrementalPage({ slug, updatedAt }) { 4 | return ( 5 | 6 |

This page has been Incrementally built.

7 |
Slug: {slug}
8 |
Updated At: {new Date(updatedAt).toLocaleTimeString()}
9 |
10 | What is happening? 11 |
    12 |
  • Next.js sends to your browser the old version of the page.
  • 13 |
  • Regenerates the page in the background.
  • 14 |
  • In the next reload, you will get the newly built page.
  • 15 |
16 |
17 | ); 18 | } 19 | 20 | export async function getStaticPaths() { 21 | return { 22 | paths: [], 23 | fallback: true, 24 | }; 25 | } 26 | 27 | export async function getStaticProps({ params }) { 28 | // You can fetch external data here 29 | return { 30 | props: { 31 | slug: params.slug, 32 | updatedAt: Date.now(), 33 | // we will attempt to re-generate the page: 34 | // - when a request comes in 35 | // - at most once every second 36 | revalidate: 1, 37 | }, 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /pages/concepts/ssg.js: -------------------------------------------------------------------------------- 1 | import Layout from "../../components/core/Layout"; 2 | import Link from "next/link"; 3 | 4 | export async function getStaticProps() { 5 | return { 6 | props: { 7 | data: "statically computed at build-time. ", 8 | }, 9 | }; 10 | } 11 | 12 | export default function Example({ data }) { 13 | return ( 14 | 15 |

Static Generation

16 |
17 |
18 | This content is: {data} 19 |
20 |
21 |
22 |

Pre-rendering

23 |

24 | By default, Next.js pre-renders every page. This 25 | means that Next.js generates HTML for each page in advance, instead of 26 | having it all done by client-side JavaScript. 27 |

28 |

Pre-rendering can result in better performance and SEO.

29 |

30 | Each generated HTML is associated with minimal JavaScript code 31 | necessary for that page. When a page is loaded by the browser, its 32 | JavaScript code runs and makes the page fully interactive. This 33 | process is called hydration. 34 |

35 |

36 | Next.js has two forms of pre-rendering:{" "} 37 | Static Generation and 38 | Server-side Rendering. The difference is in{" "} 39 | when it generates the HTML for a page. 40 |

41 |

42 | Read more about this topic:{" "} 43 |

61 |

62 |
63 |

Why static?

64 |

65 | We recommend using Static Generation over Server-side Rendering for 66 | performance reasons. Statically generated pages can be cached by CDN 67 | with no extra configuration to boost performance. However, in some 68 | cases, Server-side Rendering might be the only option. 69 |

70 |

71 | You can also use Client-side Rendering along with 72 | Static Generation. 73 |

74 |
75 |
76 | 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /pages/concepts/ssr.js: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Layout from "../../components/core/Layout"; 3 | 4 | export async function getServerSideProps() { 5 | await setTimeout(() => { 6 | // blocker function 7 | }, 1000); 8 | 9 | return { 10 | props: { 11 | data: "server side rendered.", 12 | }, 13 | }; 14 | } 15 | 16 | export default function Example({ data }) { 17 | return ( 18 | 19 |

Server Side Rendering

20 |
21 |
22 | This content is: {data} 23 |
24 |
25 |
26 |

Pre-rendering

27 |

28 | By default, Next.js pre-renders every page. This 29 | means that Next.js generates HTML for each page in advance, instead of 30 | having it all done by client-side JavaScript. 31 |

32 |

Pre-rendering can result in better performance and SEO.

33 |

34 | Each generated HTML is associated with minimal JavaScript code 35 | necessary for that page. When a page is loaded by the browser, its 36 | JavaScript code runs and makes the page fully interactive. This 37 | process is called hydration. 38 |

39 |

40 | Next.js has two forms of pre-rendering:{" "} 41 | Static Generation and 42 | Server-side Rendering. The difference is in{" "} 43 | when it generates the HTML for a page. 44 |

45 |

46 | Read more about this topic:{" "} 47 |

65 |

66 |
67 |

What happens when I use Server-side Rendering

68 |

69 | Next.js pre-renders a page on each request. It will be slower because 70 | the page cannot be cached by a CDN, but the pre-rendered page will 71 | always be up-to-date. 72 |

73 |

74 | The HTML is generated on each request. Therefore, by adding latency to 75 | the request{" "} 76 | 77 | Server-side Rendering results in slower performance than Static 78 | Generation. 79 | 80 |

81 |
82 |
83 | 90 |
91 | ); 92 | } 93 | -------------------------------------------------------------------------------- /pages/examples/common.js: -------------------------------------------------------------------------------- 1 | import Header from "../../components/examples/common/Header"; 2 | import Feed from "../../components/examples/common/Feed"; 3 | import Footer from "../../components/examples/common/Footer"; 4 | import Banner from "../../components/examples/common/Banner"; 5 | import Stats from "../../components/examples/common/Stats"; 6 | 7 | export default function Index() { 8 | return ( 9 | <> 10 | 11 |
12 |
13 | 14 | 15 |
16 |