├── .editorconfig ├── .env.example ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── README.md ├── blocks ├── ArchiveBlock │ ├── index.module.scss │ ├── index.tsx │ └── types.ts ├── CallToAction │ ├── index.module.scss │ └── index.tsx ├── Content │ ├── index.module.scss │ └── index.tsx └── MediaBlock │ ├── index.module.scss │ └── index.tsx ├── components ├── AddToCartButton │ ├── index.module.scss │ └── index.tsx ├── AdminBar │ ├── index.module.scss │ └── index.tsx ├── BackgroundColor │ ├── index.module.scss │ └── index.tsx ├── Blocks │ └── index.tsx ├── Button │ ├── index.module.scss │ └── index.tsx ├── Card │ ├── index.module.scss │ └── index.tsx ├── CartLink │ ├── index.module.scss │ └── index.tsx ├── CheckoutForm │ ├── index.module.scss │ └── index.tsx ├── CollectionArchive │ ├── index.module.scss │ └── index.tsx ├── Footer │ ├── index.module.scss │ └── index.tsx ├── Gutter │ ├── index.module.scss │ └── index.tsx ├── Header │ ├── MobileMenuModal.tsx │ ├── index.module.scss │ ├── index.tsx │ └── mobileMenuModal.module.scss ├── Hero │ ├── HighImpact │ │ ├── index.module.scss │ │ └── index.tsx │ ├── LowImpact │ │ ├── index.module.scss │ │ └── index.tsx │ ├── MediumImpact │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Product │ │ ├── index.module.scss │ │ └── index.tsx │ └── index.tsx ├── Input │ ├── index.module.scss │ └── index.tsx ├── Label │ ├── index.module.scss │ └── index.tsx ├── LargeBody │ ├── index.module.scss │ └── index.tsx ├── Link │ └── index.tsx ├── Logo │ └── index.tsx ├── Media │ ├── Image │ │ ├── index.module.scss │ │ └── index.tsx │ ├── Video │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.tsx │ └── types.ts ├── PageRange │ ├── index.module.scss │ └── index.tsx ├── PaywallBlocks │ └── index.tsx ├── Price │ ├── index.module.scss │ └── index.tsx ├── RemoveFromCartButton │ ├── index.module.scss │ └── index.tsx ├── RichText │ ├── index.module.scss │ ├── index.tsx │ └── serialize.tsx ├── VerticalPadding │ ├── index.module.scss │ └── index.tsx └── icons │ ├── Chevron │ └── index.tsx │ └── Menu │ └── index.tsx ├── css ├── app.scss ├── colors.scss ├── common.scss ├── queries.scss └── type.scss ├── cssVariables.js ├── graphql ├── blocks.ts ├── cart.ts ├── categories.ts ├── globals.ts ├── index.ts ├── link.ts ├── media.ts ├── meta.ts ├── pages.ts └── products.ts ├── next.config.js ├── package.json ├── pages ├── [slug].tsx ├── _app.tsx ├── account │ ├── index.module.css │ └── index.tsx ├── cart │ ├── index.module.scss │ └── index.tsx ├── checkout │ ├── index.module.scss │ └── index.tsx ├── create-account │ ├── index.module.css │ └── index.tsx ├── index.tsx ├── login │ ├── index.module.scss │ └── index.tsx ├── logout │ ├── index.module.scss │ └── index.tsx ├── order-confirmation │ ├── index.module.scss │ └── index.tsx ├── orders │ ├── [id] │ │ ├── index.module.scss │ │ └── index.tsx │ ├── index.module.scss │ └── index.tsx ├── products │ └── [slug].tsx ├── recover-password │ ├── index.module.scss │ └── index.tsx ├── reset-password │ ├── index.module.scss │ └── index.tsx └── styleguide │ └── index.tsx ├── payload-types.ts ├── providers ├── Auth │ └── index.tsx └── Cart │ ├── index.tsx │ └── reducer.ts ├── public └── favicon.ico ├── tsconfig.json ├── utilities ├── canUseDOM.ts ├── formatDateTime.ts └── toKebabCase.ts └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | max_line_length = null 11 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_APP_URL=http://localhost:3000 2 | NEXT_PUBLIC_CMS_URL=http://localhost:8000 3 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= 4 | STRIPE_SECRET_KEY= 5 | NEXT_PUBLIC_IS_LIVE= 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@next/next/recommended', '@payloadcms'], 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | parser: "typescript", 4 | semi: false, 5 | singleQuote: true, 6 | trailingComma: "all", 7 | arrowParens: "avoid", 8 | }; 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This repo has moved! 2 | 3 | This repo has been merged into the [Templates Directory](https://github.com/payloadcms/payload/tree/main/templates) of the [Payload Monorepo](https://github.com/payloadcms/payload). Please refer to the new location of the [E-commerce Template](https://github.com/payloadcms/payload/tree/main/templates/ecommerce) for all future updates, issues, and pull requests. 4 | -------------------------------------------------------------------------------- /blocks/ArchiveBlock/index.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../css/common'; 2 | 3 | .archiveBlock { 4 | position: relative; 5 | } 6 | 7 | .introContent { 8 | margin-bottom: calc(var(--base) * 2); 9 | 10 | @include mid-break { 11 | margin-bottom: calc(var(--base) * 2); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /blocks/ArchiveBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Cell, Grid } from '@faceless-ui/css-grid' 3 | 4 | import { CollectionArchive } from '../../components/CollectionArchive' 5 | import { Gutter } from '../../components/Gutter' 6 | import RichText from '../../components/RichText' 7 | import { ArchiveBlockProps } from './types' 8 | 9 | import classes from './index.module.scss' 10 | 11 | export const ArchiveBlock: React.FC< 12 | ArchiveBlockProps & { 13 | id?: string 14 | } 15 | > = props => { 16 | const { 17 | introContent, 18 | id, 19 | relationTo, 20 | populateBy, 21 | limit, 22 | populatedDocs, 23 | populatedDocsTotal, 24 | categories, 25 | } = props 26 | 27 | return ( 28 |
29 | {introContent && ( 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | )} 38 | 47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /blocks/ArchiveBlock/types.ts: -------------------------------------------------------------------------------- 1 | import type { Page } from '../../payload-types' 2 | 3 | export type ArchiveBlockProps = Extract 4 | -------------------------------------------------------------------------------- /blocks/CallToAction/index.module.scss: -------------------------------------------------------------------------------- 1 | @use '../../css/queries.scss' as *; 2 | 3 | $spacer-h: calc(var(--block-padding) / 2); 4 | 5 | .callToAction { 6 | padding-left: $spacer-h; 7 | padding-right: $spacer-h; 8 | } 9 | 10 | .background--white { 11 | background-color: var(--color-base-1000); 12 | color: var(--color-base-0); 13 | } 14 | 15 | .linkGroup { 16 | display: flex; 17 | flex-direction: column; 18 | justify-content: center; 19 | height: 100%; 20 | 21 | > * { 22 | margin-bottom: calc(var(--base) / 2); 23 | &:last-child { 24 | margin-bottom: 0; 25 | } 26 | } 27 | 28 | @include mid-break { 29 | padding-top: 12px 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /blocks/CallToAction/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Cell, Grid } from '@faceless-ui/css-grid'; 3 | import { Page } from '../../payload-types'; 4 | import { BackgroundColor } from '../../components/BackgroundColor'; 5 | import { Gutter } from '../../components/Gutter'; 6 | import { CMSLink } from '../../components/Link'; 7 | import RichText from '../../components/RichText'; 8 | 9 | import classes from './index.module.scss'; 10 | import { VerticalPadding } from '../../components/VerticalPadding'; 11 | 12 | type Props = Extract 13 | 14 | export const CallToActionBlock: React.FC = ({ ctaBackgroundColor, links, richText }) => { 17 | const oppositeBackgroundColor = ctaBackgroundColor === 'white' ? 'black' : 'white'; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 | 37 |
38 | {(links || []).map(({ link }, i) => { 39 | return ( 40 | 44 | ) 45 | })} 46 |
47 |
48 |
49 |
50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /blocks/Content/index.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../css/common'; 2 | 3 | .grid { 4 | row-gap: calc(var(--base) * 2) !important; 5 | 6 | @include mid-break { 7 | row-gap: var(--base) !important; 8 | } 9 | } 10 | 11 | .link { 12 | margin-top: var(--base); 13 | } 14 | -------------------------------------------------------------------------------- /blocks/Content/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Grid, Cell } from '@faceless-ui/css-grid' 3 | import { Page } from '../../payload-types'; 4 | import RichText from '../../components/RichText'; 5 | import { Gutter } from '../../components/Gutter'; 6 | import { CMSLink } from '../../components/Link'; 7 | import classes from './index.module.scss'; 8 | 9 | type Props = Extract 10 | 11 | export const ContentBlock: React.FC = (props) => { 14 | const { 15 | columns, 16 | } = props; 17 | 18 | return ( 19 | 20 | 21 | {columns && columns.length > 0 && columns.map((col, index) => { 22 | const { 23 | enableLink, 24 | richText, 25 | link, 26 | size 27 | }= col; 28 | 29 | let cols; 30 | 31 | if (size === 'oneThird') cols = 4; 32 | if (size === 'half') cols = 6; 33 | if (size === 'twoThirds') cols = 8; 34 | if (size === 'full') cols = 10; 35 | 36 | return ( 37 | 42 | 43 | {enableLink && ( 44 | 48 | )} 49 | 50 | ) 51 | })} 52 | 53 | 54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /blocks/MediaBlock/index.module.scss: -------------------------------------------------------------------------------- 1 | .mediaBlock { 2 | position: relative; 3 | } 4 | 5 | .caption { 6 | margin-top: var(--base) 7 | } 8 | -------------------------------------------------------------------------------- /blocks/MediaBlock/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Gutter } from '../../components/Gutter'; 3 | import { Media } from '../../components/Media'; 4 | import { Page } from '../../payload-types'; 5 | import RichText from '../../components/RichText'; 6 | import classes from './index.module.scss'; 7 | 8 | type Props = Extract 9 | 10 | export const MediaBlock: React.FC = (props) => { 13 | const { 14 | media, 15 | position = 'default', 16 | } = props; 17 | 18 | let caption; 19 | if (media && typeof media === 'object') caption = media.caption; 20 | 21 | return ( 22 |
23 | {position === 'fullscreen' && ( 24 |
25 | 28 |
29 | )} 30 | {position === 'default' && ( 31 | 32 |
33 | 36 |
37 |
38 | )} 39 | {caption && ( 40 | 41 | 42 | 43 | )} 44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /components/AddToCartButton/index.module.scss: -------------------------------------------------------------------------------- 1 | .addToCartButton { 2 | // cursor: pointer; 3 | // background-color: transparent; 4 | // padding: 0; 5 | // border: none; 6 | // font-size: inherit; 7 | // line-height: inherit; 8 | // text-decoration: underline; 9 | // white-space: nowrap; 10 | } 11 | -------------------------------------------------------------------------------- /components/AddToCartButton/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react' 2 | 3 | import { Product } from '../../payload-types' 4 | import { useCart } from '../../providers/Cart' 5 | import { Button, Props } from '../Button' 6 | 7 | import classes from './index.module.scss' 8 | 9 | export const AddToCartButton: React.FC<{ 10 | product: Product 11 | quantity?: number 12 | className?: string 13 | appearance?: Props['appearance'] 14 | }> = props => { 15 | const { product, quantity = 1, className, appearance = 'primary' } = props 16 | 17 | const { cart, addItemToCart, isProductInCart } = useCart() 18 | 19 | const [showInCart, setShowInCart] = useState() 20 | 21 | useEffect(() => { 22 | setShowInCart(isProductInCart(product)) 23 | }, [isProductInCart, product, cart]) 24 | 25 | if (showInCart) { 26 | return ( 27 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /components/RichText/index.module.scss: -------------------------------------------------------------------------------- 1 | .richText { 2 | :first-child { 3 | margin-top: 0; 4 | } 5 | :last-child { 6 | margin-bottom: 0; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /components/RichText/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import serialize from './serialize' 4 | 5 | import classes from './index.module.scss' 6 | 7 | const RichText: React.FC<{ className?: string; content: any }> = ({ className, content }) => { 8 | if (!content) { 9 | return null 10 | } 11 | 12 | return ( 13 |
14 | {serialize(content)} 15 |
16 | ) 17 | } 18 | 19 | export default RichText 20 | -------------------------------------------------------------------------------- /components/RichText/serialize.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import escapeHTML from 'escape-html' 3 | import { Text } from 'slate' 4 | 5 | import { Label } from '../Label' 6 | import { LargeBody } from '../LargeBody' 7 | 8 | // eslint-disable-next-line no-use-before-define 9 | type Children = Leaf[] 10 | 11 | type Leaf = { 12 | type: string 13 | value?: { 14 | url: string 15 | alt: string 16 | } 17 | children?: Children 18 | url?: string 19 | [key: string]: unknown 20 | } 21 | 22 | const serialize = (children: Children): React.ReactElement[] => 23 | children.map((node, i) => { 24 | if (Text.isText(node)) { 25 | let text = 26 | 27 | if (node.bold) { 28 | text = {text} 29 | } 30 | 31 | if (node.code) { 32 | text = {text} 33 | } 34 | 35 | if (node.italic) { 36 | text = {text} 37 | } 38 | 39 | if (node.underline) { 40 | text = ( 41 | 42 | {text} 43 | 44 | ) 45 | } 46 | 47 | if (node.strikethrough) { 48 | text = ( 49 | 50 | {text} 51 | 52 | ) 53 | } 54 | 55 | return {text} 56 | } 57 | 58 | if (!node) { 59 | return null 60 | } 61 | 62 | switch (node.type) { 63 | case 'h1': 64 | return

{serialize(node.children)}

65 | case 'h2': 66 | return

{serialize(node.children)}

67 | case 'h3': 68 | return

{serialize(node.children)}

69 | case 'h4': 70 | return

{serialize(node.children)}

71 | case 'h5': 72 | return
{serialize(node.children)}
73 | case 'h6': 74 | return
{serialize(node.children)}
75 | case 'quote': 76 | return
{serialize(node.children)}
77 | case 'ul': 78 | return
    {serialize(node.children)}
79 | case 'ol': 80 | return
    {serialize(node.children)}
81 | case 'li': 82 | return
  • {serialize(node.children)}
  • 83 | case 'link': 84 | return ( 85 | 95 | {serialize(node.children)} 96 | 97 | ) 98 | 99 | case 'label': 100 | return 101 | 102 | case 'large-body': { 103 | return {serialize(node.children)} 104 | } 105 | 106 | default: 107 | return

    {serialize(node.children)}

    108 | } 109 | }) 110 | 111 | export default serialize 112 | -------------------------------------------------------------------------------- /components/VerticalPadding/index.module.scss: -------------------------------------------------------------------------------- 1 | .top-large { 2 | padding-top: var(--block-padding); 3 | } 4 | 5 | .top-medium { 6 | padding-top: calc(var(--block-padding) / 2); 7 | } 8 | 9 | .bottom-large { 10 | padding-bottom: var(--block-padding); 11 | } 12 | 13 | .bottom-medium { 14 | padding-bottom: calc(var(--block-padding) / 2); 15 | } -------------------------------------------------------------------------------- /components/VerticalPadding/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import classes from './index.module.scss' 4 | 5 | export type VerticalPaddingOptions = 'large' | 'medium' | 'none' 6 | 7 | type Props = { 8 | top?: VerticalPaddingOptions 9 | bottom?: VerticalPaddingOptions 10 | children: React.ReactNode 11 | className?: string 12 | } 13 | 14 | export const VerticalPadding: React.FC = ({ 15 | top = 'medium', 16 | bottom = 'medium', 17 | className, 18 | children, 19 | }) => { 20 | return ( 21 |
    26 | {children} 27 |
    28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /components/icons/Chevron/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const Chevron: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /components/icons/Menu/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const MenuIcon: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /css/app.scss: -------------------------------------------------------------------------------- 1 | @use './queries.scss' as *; 2 | @use './colors.scss' as *; 3 | @use './type.scss' as *; 4 | 5 | :root { 6 | --breakpoint-xs-width : #{$breakpoint-xs-width}; 7 | --breakpoint-s-width : #{$breakpoint-s-width}; 8 | --breakpoint-m-width : #{$breakpoint-m-width}; 9 | --breakpoint-l-width : #{$breakpoint-l-width}; 10 | --scrollbar-width: 17px; 11 | 12 | --base: 24px; 13 | --font-body: system-ui; 14 | --font-mono: 'Roboto Mono', monospace; 15 | 16 | --gutter-h: 180px; 17 | --block-padding: 120px; 18 | 19 | --header-z-index: 100; 20 | --modal-z-index: 90; 21 | 22 | @include large-break { 23 | --gutter-h: 144px; 24 | --block-padding: 96px; 25 | } 26 | 27 | @include mid-break { 28 | --gutter-h: 24px; 29 | --block-padding: 60px; 30 | } 31 | } 32 | 33 | ///////////////////////////// 34 | // GLOBAL STYLES 35 | ///////////////////////////// 36 | 37 | * { 38 | box-sizing: border-box; 39 | } 40 | 41 | html { 42 | @extend %body; 43 | background: var(--color-base-0); 44 | -webkit-font-smoothing: antialiased; 45 | } 46 | 47 | html, 48 | body, 49 | #app { 50 | height: 100%; 51 | } 52 | 53 | body { 54 | font-family: var(--font-body); 55 | color: var(--color-base-1000); 56 | margin: 0; 57 | } 58 | 59 | ::selection { 60 | background: var(--color-success-500); 61 | color: var(--color-base-800); 62 | } 63 | 64 | ::-moz-selection { 65 | background: var(--color-success-500); 66 | color: var(--color-base-800); 67 | } 68 | 69 | img { 70 | max-width: 100%; 71 | height: auto; 72 | display: block; 73 | } 74 | 75 | h1 { 76 | @extend %h1; 77 | } 78 | 79 | h2 { 80 | @extend %h2; 81 | } 82 | 83 | h3 { 84 | @extend %h3; 85 | } 86 | 87 | h4 { 88 | @extend %h4; 89 | } 90 | 91 | h5 { 92 | @extend %h5; 93 | } 94 | 95 | h6 { 96 | @extend %h6; 97 | } 98 | 99 | p { 100 | margin: var(--base) 0; 101 | 102 | @include mid-break { 103 | margin: calc(var(--base) * .75) 0; 104 | } 105 | } 106 | 107 | ul, 108 | ol { 109 | padding-left: var(--base); 110 | margin: 0 0 var(--base); 111 | } 112 | 113 | a { 114 | color: currentColor; 115 | 116 | &:focus { 117 | opacity: .8; 118 | outline: none; 119 | } 120 | 121 | &:active { 122 | opacity: .7; 123 | outline: none; 124 | } 125 | } 126 | 127 | svg { 128 | vertical-align: middle; 129 | } -------------------------------------------------------------------------------- /css/colors.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --color-base-0: rgb(255, 255, 255); 3 | --color-base-50: rgb(245, 245, 245); 4 | --color-base-100: rgb(235, 235, 235); 5 | --color-base-150: rgb(221, 221, 221); 6 | --color-base-200: rgb(208, 208, 208); 7 | --color-base-250: rgb(195, 195, 195); 8 | --color-base-300: rgb(181, 181, 181); 9 | --color-base-350: rgb(168, 168, 168); 10 | --color-base-400: rgb(154, 154, 154); 11 | --color-base-450: rgb(141, 141, 141); 12 | --color-base-500: rgb(128, 128, 128); 13 | --color-base-550: rgb(114, 114, 114); 14 | --color-base-600: rgb(101, 101, 101); 15 | --color-base-650: rgb(87, 87, 87); 16 | --color-base-700: rgb(74, 74, 74); 17 | --color-base-750: rgb(60, 60, 60); 18 | --color-base-800: rgb(47, 47, 47); 19 | --color-base-850: rgb(34, 34, 34); 20 | --color-base-900: rgb(20, 20, 20); 21 | --color-base-950: rgb(7, 7, 7); 22 | --color-base-1000: rgb(0, 0, 0); 23 | 24 | --color-success-50: rgb(247, 255, 251); 25 | --color-success-100: rgb(240, 255, 247); 26 | --color-success-150: rgb(232, 255, 243); 27 | --color-success-200: rgb(224, 255, 239); 28 | --color-success-250: rgb(217, 255, 235); 29 | --color-success-300: rgb(209, 255, 230); 30 | --color-success-350: rgb(201, 255, 226); 31 | --color-success-400: rgb(193, 255, 222); 32 | --color-success-450: rgb(186, 255, 218); 33 | --color-success-500: rgb(178, 255, 214); 34 | --color-success-550: rgb(160, 230, 193); 35 | --color-success-600: rgb(142, 204, 171); 36 | --color-success-650: rgb(125, 179, 150); 37 | --color-success-700: rgb(107, 153, 128); 38 | --color-success-750: rgb(89, 128, 107); 39 | --color-success-800: rgb(71, 102, 86); 40 | --color-success-850: rgb(53, 77, 64); 41 | --color-success-900: rgb(36, 51, 43); 42 | --color-success-950: rgb(18, 25, 21); 43 | 44 | --color-warning-50: rgb(255, 255, 246); 45 | --color-warning-100: rgb(255, 255, 237); 46 | --color-warning-150: rgb(254, 255, 228); 47 | --color-warning-200: rgb(254, 255, 219); 48 | --color-warning-250: rgb(254, 255, 210); 49 | --color-warning-300: rgb(254, 255, 200); 50 | --color-warning-350: rgb(254, 255, 191); 51 | --color-warning-400: rgb(253, 255, 182); 52 | --color-warning-450: rgb(253, 255, 173); 53 | --color-warning-500: rgb(253, 255, 164); 54 | --color-warning-550: rgb(228, 230, 148); 55 | --color-warning-600: rgb(202, 204, 131); 56 | --color-warning-650: rgb(177, 179, 115); 57 | --color-warning-700: rgb(152, 153, 98); 58 | --color-warning-750: rgb(127, 128, 82); 59 | --color-warning-800: rgb(101, 102, 66); 60 | --color-warning-850: rgb(76, 77, 49); 61 | --color-warning-900: rgb(51, 51, 33); 62 | --color-warning-950: rgb(25, 25, 16); 63 | 64 | --color-error-50: rgb(255, 241, 241); 65 | --color-error-100: rgb(255, 226, 228); 66 | --color-error-150: rgb(255, 212, 214); 67 | --color-error-200: rgb(255, 197, 200); 68 | --color-error-250: rgb(255, 183, 187); 69 | --color-error-300: rgb(255, 169, 173); 70 | --color-error-350: rgb(255, 154, 159); 71 | --color-error-400: rgb(255, 140, 145); 72 | --color-error-450: rgb(255, 125, 132); 73 | --color-error-500: rgb(255, 111, 118); 74 | --color-error-550: rgb(230, 100, 106); 75 | --color-error-600: rgb(204, 89, 94); 76 | --color-error-650: rgb(179, 78, 83); 77 | --color-error-700: rgb(153, 67, 71); 78 | --color-error-750: rgb(128, 56, 59); 79 | --color-error-800: rgb(102, 44, 47); 80 | --color-error-850: rgb(77, 33, 35); 81 | --color-error-900: rgb(51, 22, 24); 82 | --color-error-950: rgb(25, 11, 12); 83 | } -------------------------------------------------------------------------------- /css/common.scss: -------------------------------------------------------------------------------- 1 | @forward './queries.scss'; 2 | @forward './type.scss'; -------------------------------------------------------------------------------- /css/queries.scss: -------------------------------------------------------------------------------- 1 | $breakpoint-xs-width: 400px; 2 | $breakpoint-s-width: 768px; 3 | $breakpoint-m-width: 1024px; 4 | $breakpoint-l-width: 1440px; 5 | 6 | //////////////////////////// 7 | // MEDIA QUERIES 8 | ///////////////////////////// 9 | 10 | @mixin extra-small-break { 11 | @media (max-width: #{$breakpoint-xs-width}) { 12 | @content; 13 | } 14 | } 15 | 16 | @mixin small-break { 17 | @media (max-width: #{$breakpoint-s-width}) { 18 | @content; 19 | } 20 | } 21 | 22 | @mixin mid-break { 23 | @media (max-width: #{$breakpoint-m-width}) { 24 | @content; 25 | } 26 | } 27 | 28 | @mixin large-break { 29 | @media (max-width: #{$breakpoint-l-width}) { 30 | @content; 31 | } 32 | } -------------------------------------------------------------------------------- /css/type.scss: -------------------------------------------------------------------------------- 1 | @use 'queries' as *; 2 | 3 | ///////////////////////////// 4 | // HEADINGS 5 | ///////////////////////////// 6 | 7 | %h1, 8 | %h2, 9 | %h3, 10 | %h4, 11 | %h5, 12 | %h6 { 13 | font-weight: 700; 14 | } 15 | 16 | %h1 { 17 | margin: 40px 0; 18 | font-size: 64px; 19 | line-height: 70px; 20 | font-weight: bold; 21 | 22 | @include mid-break { 23 | margin: 24px 0; 24 | font-size: 42px; 25 | line-height: 42px; 26 | } 27 | } 28 | 29 | %h2 { 30 | margin: 28px 0; 31 | font-size: 48px; 32 | line-height: 54px; 33 | font-weight: bold; 34 | 35 | @include mid-break { 36 | margin: 22px 0; 37 | font-size: 32px; 38 | line-height: 40px; 39 | } 40 | } 41 | 42 | %h3 { 43 | margin: 24px 0; 44 | font-size: 32px; 45 | line-height: 40px; 46 | font-weight: bold; 47 | 48 | @include mid-break { 49 | margin: 20px 0; 50 | font-size: 26px; 51 | line-height: 32px; 52 | } 53 | } 54 | 55 | %h4 { 56 | margin: 20px 0; 57 | font-size: 26px; 58 | line-height: 32px; 59 | font-weight: bold; 60 | 61 | @include mid-break { 62 | font-size: 22px; 63 | line-height: 30px; 64 | } 65 | } 66 | 67 | %h5 { 68 | margin: 20px 0; 69 | font-size: 22px; 70 | line-height: 30px; 71 | font-weight: bold; 72 | 73 | @include mid-break { 74 | font-size: 18px; 75 | line-height: 24px; 76 | } 77 | } 78 | 79 | %h6 { 80 | margin: 20px 0; 81 | font-size: inherit; 82 | line-height: inherit; 83 | font-weight: bold; 84 | } 85 | 86 | ///////////////////////////// 87 | // TYPE STYLES 88 | ///////////////////////////// 89 | 90 | %body { 91 | font-size: 18px; 92 | line-height: 32px; 93 | 94 | @include mid-break { 95 | font-size: 15px; 96 | line-height: 24px; 97 | } 98 | } 99 | 100 | %large-body { 101 | font-size: 25px; 102 | line-height: 32px; 103 | 104 | @include mid-break { 105 | font-size: 22px; 106 | line-height: 30px; 107 | } 108 | } 109 | 110 | %label { 111 | font-size: 16px; 112 | line-height: 24px; 113 | letter-spacing: 1px; 114 | text-transform: uppercase; 115 | 116 | @include mid-break { 117 | font-size: 13px; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /cssVariables.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | breakpoints: { 3 | s: 768, 4 | m: 1024, 5 | l: 1679, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /graphql/blocks.ts: -------------------------------------------------------------------------------- 1 | import { CATEGORIES } from "./categories"; 2 | import { LINK_FIELDS } from "./link"; 3 | import { MEDIA } from "./media"; 4 | import { META } from "./meta"; 5 | 6 | export const CALL_TO_ACTION = ` 7 | ...on Cta { 8 | blockType 9 | ctaBackgroundColor 10 | richText 11 | links { 12 | link ${LINK_FIELDS()} 13 | } 14 | } 15 | ` 16 | 17 | export const CONTENT = ` 18 | ...on Content { 19 | blockType 20 | backgroundColor 21 | columns { 22 | size 23 | richText 24 | enableLink 25 | link ${LINK_FIELDS()} 26 | } 27 | } 28 | ` 29 | 30 | export const MEDIA_BLOCK = ` 31 | ...on MediaBlock { 32 | blockType 33 | mediaBlockBackgroundColor 34 | position 35 | ${MEDIA} 36 | } 37 | ` 38 | 39 | export const ARCHIVE_BLOCK = ` 40 | ...on Archive { 41 | blockType 42 | introContent 43 | populateBy 44 | relationTo 45 | ${CATEGORIES} 46 | limit 47 | selectedDocs { 48 | relationTo 49 | value { 50 | ...on Product { 51 | id 52 | slug 53 | title 54 | priceJSON 55 | } 56 | } 57 | } 58 | populatedDocs { 59 | relationTo 60 | value { 61 | ...on Product { 62 | id 63 | slug 64 | title 65 | priceJSON 66 | ${CATEGORIES} 67 | ${META} 68 | } 69 | } 70 | } 71 | populatedDocsTotal 72 | } 73 | ` 74 | -------------------------------------------------------------------------------- /graphql/cart.ts: -------------------------------------------------------------------------------- 1 | import { META } from "./meta"; 2 | 3 | export const CART = `cart { 4 | items { 5 | product { 6 | id 7 | slug 8 | priceJSON 9 | ${META} 10 | } 11 | quantity 12 | } 13 | }` 14 | -------------------------------------------------------------------------------- /graphql/categories.ts: -------------------------------------------------------------------------------- 1 | export const CATEGORIES = `categories { 2 | title 3 | id 4 | breadcrumbs { 5 | id 6 | label 7 | } 8 | }` 9 | -------------------------------------------------------------------------------- /graphql/globals.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '@apollo/client'; 2 | import { ARCHIVE_BLOCK, CALL_TO_ACTION, CONTENT, MEDIA_BLOCK } from './blocks'; 3 | import { LINK_FIELDS } from './link'; 4 | import { MEDIA } from './media'; 5 | 6 | export const HEADER = ` 7 | Header { 8 | navItems { 9 | link ${LINK_FIELDS({ disableAppearance: true })} 10 | } 11 | } 12 | `; 13 | 14 | export const HEADER_QUERY = gql` 15 | query Header { 16 | ${HEADER} 17 | } 18 | ` 19 | 20 | export const FOOTER = ` 21 | Header { 22 | navItems { 23 | link ${LINK_FIELDS({ disableAppearance: true })} 24 | } 25 | } 26 | `; 27 | 28 | export const FOOTER_QUERY = gql` 29 | query Header { 30 | ${FOOTER} 31 | } 32 | ` 33 | 34 | export const SETTINGS = ` 35 | Settings { 36 | shopPage { 37 | slug 38 | } 39 | } 40 | `; 41 | 42 | export const SETTINGS_QUERY = gql` 43 | query Settings { 44 | ${SETTINGS} 45 | } 46 | ` 47 | -------------------------------------------------------------------------------- /graphql/index.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, InMemoryCache } from "@apollo/client"; 2 | 3 | let CLIENT: ApolloClient 4 | 5 | // By re-using the client if `NODE_ENV === 'production'`, 6 | // we'll leverage Apollo caching 7 | // to reduce the calls made to commonly needed assets 8 | // like MainMenu, Footer, etc. 9 | 10 | export function getApolloClient() { 11 | if (!CLIENT || process.env.NODE_ENV !== 'production') { 12 | CLIENT = new ApolloClient({ 13 | ssrMode: true, 14 | uri: `${process.env.NEXT_PUBLIC_CMS_URL}/api/graphql`, 15 | cache: new InMemoryCache(), 16 | }); 17 | } 18 | 19 | return CLIENT; 20 | } 21 | -------------------------------------------------------------------------------- /graphql/link.ts: -------------------------------------------------------------------------------- 1 | type Args = { 2 | disableLabel?: true 3 | disableAppearance?: true 4 | } 5 | 6 | export const LINK_FIELDS = ({ disableAppearance, disableLabel }: Args = {}) => `{ 7 | ${!disableLabel ? 'label' : ''} 8 | ${!disableAppearance ? 'appearance' : ''} 9 | type 10 | newTab 11 | url 12 | reference { 13 | relationTo 14 | value { 15 | ...on Page { 16 | slug 17 | } 18 | } 19 | } 20 | }` -------------------------------------------------------------------------------- /graphql/media.ts: -------------------------------------------------------------------------------- 1 | export const MEDIA_FIELDS = ` 2 | mimeType 3 | filename 4 | width 5 | height 6 | alt 7 | caption 8 | ` 9 | 10 | export const MEDIA = `media { 11 | ${MEDIA_FIELDS} 12 | }` 13 | -------------------------------------------------------------------------------- /graphql/meta.ts: -------------------------------------------------------------------------------- 1 | import { MEDIA_FIELDS } from "./media"; 2 | 3 | export const META = `meta { 4 | title 5 | image { 6 | ${MEDIA_FIELDS} 7 | } 8 | description 9 | }` 10 | -------------------------------------------------------------------------------- /graphql/pages.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | import { ARCHIVE_BLOCK, CALL_TO_ACTION, CONTENT, MEDIA_BLOCK } from "./blocks"; 3 | import { HEADER, FOOTER, SETTINGS } from "./globals"; 4 | import { LINK_FIELDS } from "./link"; 5 | import { MEDIA } from "./media"; 6 | import { META } from "./meta"; 7 | 8 | export const PAGES = gql` 9 | query Pages { 10 | Pages(limit: 300, where: { slug: { not_equals: "cart" } }) { 11 | docs { 12 | slug 13 | } 14 | } 15 | } 16 | ` 17 | 18 | export const PAGE = gql` 19 | query Page($slug: String ) { 20 | Pages(where: { AND: [{ slug: { equals: $slug }}] }) { 21 | docs { 22 | id 23 | title 24 | hero { 25 | type 26 | richText 27 | links { 28 | link ${LINK_FIELDS()} 29 | } 30 | ${MEDIA} 31 | } 32 | layout { 33 | ${CONTENT} 34 | ${CALL_TO_ACTION} 35 | ${CONTENT} 36 | ${MEDIA_BLOCK} 37 | ${ARCHIVE_BLOCK} 38 | } 39 | ${META} 40 | } 41 | } 42 | ${HEADER} 43 | ${FOOTER} 44 | ${SETTINGS} 45 | } 46 | ` 47 | -------------------------------------------------------------------------------- /graphql/products.ts: -------------------------------------------------------------------------------- 1 | import { gql } from "@apollo/client"; 2 | import { ARCHIVE_BLOCK, CALL_TO_ACTION, CONTENT, MEDIA_BLOCK } from "./blocks"; 3 | import { CATEGORIES } from "./categories"; 4 | import { HEADER, FOOTER, SETTINGS } from "./globals"; 5 | import { META } from "./meta"; 6 | 7 | export const PRODUCTS = gql` 8 | query Products { 9 | Products(limit: 300) { 10 | docs { 11 | slug 12 | } 13 | } 14 | } 15 | ` 16 | 17 | export const PRODUCT = gql` 18 | query Product($slug: String ) { 19 | Products(where: { slug: { equals: $slug}}) { 20 | docs { 21 | id 22 | title 23 | ${CATEGORIES} 24 | layout { 25 | ${CALL_TO_ACTION} 26 | ${CONTENT} 27 | ${MEDIA_BLOCK} 28 | ${ARCHIVE_BLOCK} 29 | } 30 | paywall { 31 | ${CALL_TO_ACTION} 32 | ${CONTENT} 33 | ${MEDIA_BLOCK} 34 | ${ARCHIVE_BLOCK} 35 | } 36 | priceJSON 37 | ${META} 38 | } 39 | } 40 | ${HEADER} 41 | ${FOOTER} 42 | ${SETTINGS} 43 | } 44 | ` 45 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | domains: [ 7 | 'localhost', 8 | process.env.NEXT_PUBLIC_CMS_URL 9 | ], 10 | // remotePatterns: [ 11 | // { 12 | // protocol: 'https', 13 | // hostname: 'localhost', 14 | // port: '3000', 15 | // pathname: '/media/**', 16 | // }, 17 | // ], 18 | }, 19 | async headers() { 20 | const headers = [] 21 | 22 | if (!process.env.NEXT_PUBLIC_IS_LIVE) { 23 | headers.push({ 24 | headers: [ 25 | { 26 | key: 'X-Robots-Tag', 27 | value: 'noindex', 28 | }, 29 | ], 30 | source: '/:path*', 31 | }) 32 | } 33 | return headers 34 | }, 35 | } 36 | 37 | module.exports = nextConfig 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@payloadcms/template-ecommerce-nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@apollo/client": "^3.7.0", 13 | "@faceless-ui/css-grid": "^1.2.0", 14 | "@faceless-ui/modal": "^2.0.1", 15 | "@stripe/react-stripe-js": "^1.16.3", 16 | "@stripe/stripe-js": "^1.46.0", 17 | "apollo-link-http": "^1.5.17", 18 | "escape-html": "^1.0.3", 19 | "graphql": "^16.6.0", 20 | "next": "^13.1.2", 21 | "payload-admin-bar": "^1.0.5", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "react-hook-form": "^7.41.5", 25 | "sass": "^1.55.0", 26 | "slate": "^0.84.0", 27 | "stripe": "^11.6.0" 28 | }, 29 | "devDependencies": { 30 | "@next/eslint-plugin-next": "^13.1.6", 31 | "@types/node": "18.11.3", 32 | "@types/react": "18.0.21", 33 | "@typescript-eslint/eslint-plugin": "^5.51.0", 34 | "@typescript-eslint/parser": "^5.51.0", 35 | "@payloadcms/eslint-config": "^0.0.1", 36 | "eslint": "8.25.0", 37 | "eslint-config-prettier": "^8.5.0", 38 | "eslint-plugin-filenames": "^1.3.2", 39 | "eslint-plugin-import": "2.25.4", 40 | "eslint-plugin-prettier": "^4.0.0", 41 | "eslint-plugin-react-hooks": "^4.6.0", 42 | "eslint-plugin-simple-import-sort": "^10.0.0", 43 | "prettier": "^2.7.1", 44 | "typescript": "4.8.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pages/[slug].tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GetStaticPaths, GetStaticProps } from 'next' 3 | 4 | import { Blocks } from '../components/Blocks' 5 | import { Hero } from '../components/Hero' 6 | import { getApolloClient } from '../graphql' 7 | import { PAGE, PAGES } from '../graphql/pages' 8 | import type { Page } from '../payload-types' 9 | 10 | const PageTemplate: React.FC<{ 11 | page: Page 12 | preview?: boolean 13 | }> = props => { 14 | const { page } = props 15 | 16 | if (page) { 17 | const { hero, layout } = page 18 | 19 | return ( 20 | 21 | 22 | 26 | 27 | ) 28 | } 29 | 30 | return null 31 | } 32 | 33 | export const getStaticProps: GetStaticProps = async ({ params }) => { 34 | const apolloClient = getApolloClient() 35 | const slug = params?.slug || 'home' 36 | 37 | const { data } = await apolloClient.query({ 38 | query: PAGE, 39 | variables: { 40 | slug, 41 | }, 42 | }) 43 | 44 | if (!data.Pages.docs[0]) { 45 | return { 46 | notFound: true, 47 | } 48 | } 49 | 50 | return { 51 | props: { 52 | page: data?.Pages?.docs?.[0] || null, 53 | header: data?.Header || null, 54 | footer: data?.Footer || null, 55 | collection: 'pages', 56 | id: data?.Pages?.docs?.[0]?.id || null, 57 | }, 58 | } 59 | } 60 | 61 | export const getStaticPaths: GetStaticPaths = async () => { 62 | const apolloClient = getApolloClient() 63 | 64 | const { data } = await apolloClient.query({ 65 | query: PAGES, 66 | }) 67 | 68 | return { 69 | paths: data.Pages.docs.map(({ slug }) => ({ 70 | params: { slug }, 71 | })), 72 | fallback: 'blocking', 73 | } 74 | } 75 | 76 | export default PageTemplate 77 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback } from 'react' 2 | import { GridProvider } from '@faceless-ui/css-grid' 3 | import { ModalContainer, ModalProvider } from '@faceless-ui/modal' 4 | import { AppProps } from 'next/app' 5 | import { useRouter } from 'next/router' 6 | 7 | import { AdminBar } from '../components/AdminBar' 8 | import { Footer } from '../components/Footer' 9 | import { Header } from '../components/Header' 10 | import cssVariables from '../cssVariables' 11 | import { Footer as FooterType, Header as HeaderType } from '../payload-types' 12 | import { AuthProvider } from '../providers/Auth' 13 | import { CartProvider } from '../providers/Cart' 14 | 15 | import '../css/app.scss' 16 | 17 | const PayloadApp = ( 18 | appProps: AppProps<{ 19 | id: string 20 | preview: boolean 21 | collection: string 22 | header: HeaderType 23 | footer: FooterType 24 | }>, 25 | ): React.ReactElement => { 26 | const { Component, pageProps } = appProps 27 | 28 | const { collection, id, preview } = pageProps 29 | 30 | const router = useRouter() 31 | 32 | const onPreviewExit = useCallback(() => { 33 | const exit = async () => { 34 | const exitReq = await fetch('/api/exit-preview') 35 | if (exitReq.status === 200) { 36 | router.reload() 37 | } 38 | } 39 | exit() 40 | }, [router]) 41 | 42 | return ( 43 | 44 | 45 | 64 | 65 | 73 |
    74 | 75 |