├── .nvmrc ├── .prettierignore ├── app ├── favicon.ico ├── opengraph-image.tsx ├── globals.css ├── error.tsx ├── robots.ts ├── search │ ├── loading.tsx │ ├── [collection] │ │ ├── opengraph-image.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── [page] │ ├── opengraph-image.tsx │ ├── layout.tsx │ └── page.tsx ├── page.tsx ├── layout.tsx ├── sitemap.ts ├── api │ └── revalidate │ │ └── route.ts └── product │ └── [handle] │ └── page.tsx ├── fonts └── Inter-Bold.ttf ├── postcss.config.js ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── e2e.yml ├── lib ├── shopify │ ├── fragments │ │ ├── seo.ts │ │ ├── image.ts │ │ ├── cart.ts │ │ └── product.ts │ ├── queries │ │ ├── menu.ts │ │ ├── cart.ts │ │ ├── page.ts │ │ ├── product.ts │ │ └── collection.ts │ ├── mutations │ │ └── cart.ts │ ├── types.ts │ └── index.ts ├── utils.ts ├── type-guards.ts └── constants.ts ├── .env.example ├── .vscode ├── settings.json └── launch.json ├── prettier.config.js ├── components ├── icons │ ├── arrow-left.tsx │ ├── search.tsx │ ├── minus.tsx │ ├── menu.tsx │ ├── plus.tsx │ ├── close.tsx │ ├── caret-right.tsx │ ├── logo.tsx │ ├── cart.tsx │ ├── shopping-bag.tsx │ ├── github.tsx │ └── vercel.tsx ├── price.tsx ├── loading-dots.tsx ├── grid │ ├── index.tsx │ ├── three-items.tsx │ └── tile.tsx ├── cart │ ├── index.tsx │ ├── delete-item-button.tsx │ ├── actions.ts │ ├── add-to-cart.tsx │ ├── edit-item-quantity-button.tsx │ └── modal.tsx ├── prose.tsx ├── layout │ ├── product-grid-items.tsx │ ├── search │ │ ├── filter │ │ │ ├── index.tsx │ │ │ ├── dropdown.tsx │ │ │ └── item.tsx │ │ └── collections.tsx │ ├── navbar │ │ ├── search.tsx │ │ ├── index.tsx │ │ └── mobile-menu.tsx │ └── footer.tsx ├── opengraph-image.tsx ├── carousel.tsx └── product │ ├── gallery.tsx │ └── variant-selector.tsx ├── .eslintrc.js ├── next.config.js ├── .gitignore ├── e2e ├── mobile-menu.spec.ts └── cart.spec.ts ├── tsconfig.json ├── playwright.config.ts ├── license.md ├── package.json ├── tailwind.config.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .next 3 | pnpm-lock.yaml 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuilderIO/commerce/main/app/favicon.ico -------------------------------------------------------------------------------- /fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BuilderIO/commerce/main/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /lib/shopify/fragments/seo.ts: -------------------------------------------------------------------------------- 1 | const seoFragment = /* GraphQL */ ` 2 | fragment seo on SEO { 3 | description 4 | title 5 | } 6 | `; 7 | 8 | export default seoFragment; 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | TWITTER_CREATOR="@vercel" 2 | TWITTER_SITE="https://nextjs.org/commerce" 3 | SITE_NAME="Next.js Commerce" 4 | SHOPIFY_REVALIDATION_SECRET= 5 | SHOPIFY_STOREFRONT_ACCESS_TOKEN= 6 | SHOPIFY_STORE_DOMAIN= 7 | -------------------------------------------------------------------------------- /app/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import OpengraphImage from 'components/opengraph-image'; 2 | 3 | export const runtime = 'edge'; 4 | 5 | export default async function Image() { 6 | return await OpengraphImage(); 7 | } 8 | -------------------------------------------------------------------------------- /lib/shopify/fragments/image.ts: -------------------------------------------------------------------------------- 1 | const imageFragment = /* GraphQL */ ` 2 | fragment image on Image { 3 | url 4 | altText 5 | width 6 | height 7 | } 8 | `; 9 | 10 | export default imageFragment; 11 | -------------------------------------------------------------------------------- /lib/shopify/queries/menu.ts: -------------------------------------------------------------------------------- 1 | export const getMenuQuery = /* GraphQL */ ` 2 | query getMenu($handle: String!) { 3 | menu(handle: $handle) { 4 | items { 5 | title 6 | url 7 | } 8 | } 9 | } 10 | `; 11 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @supports (font: -apple-system-body) and (-webkit-appearance: none) { 6 | img[loading='lazy'] { 7 | clip-path: inset(0.6px); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/shopify/queries/cart.ts: -------------------------------------------------------------------------------- 1 | import cartFragment from '../fragments/cart'; 2 | 3 | export const getCartQuery = /* GraphQL */ ` 4 | query getCart($cartId: ID!) { 5 | cart(id: $cartId) { 6 | ...cart 7 | } 8 | } 9 | ${cartFragment} 10 | `; 11 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | export default function Error({ reset }: { reset: () => void }) { 4 | return ( 5 |
6 |

Something went wrong.

7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll": true, 6 | "source.organizeImports": true, 7 | "source.sortMembers": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ReadonlyURLSearchParams } from 'next/navigation'; 2 | 3 | export const createUrl = (pathname: string, params: URLSearchParams | ReadonlyURLSearchParams) => { 4 | const paramsString = params.toString(); 5 | const queryString = `${paramsString.length ? '?' : ''}${paramsString}`; 6 | 7 | return `${pathname}${queryString}`; 8 | }; 9 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | arrowParens: 'always', 4 | trailingComma: 'none', 5 | printWidth: 100, 6 | tabWidth: 2, 7 | // pnpm doesn't support plugin autoloading 8 | // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#installation 9 | plugins: [require('prettier-plugin-tailwindcss')] 10 | }; 11 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | const baseUrl = process.env.NEXT_PUBLIC_VERCEL_URL 2 | ? `https://${process.env.NEXT_PUBLIC_VERCEL_URL}` 3 | : 'http://localhost:3000'; 4 | 5 | export default function robots() { 6 | return { 7 | rules: [ 8 | { 9 | userAgent: '*' 10 | } 11 | ], 12 | sitemap: `${baseUrl}/sitemap.xml`, 13 | host: baseUrl 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /app/search/loading.tsx: -------------------------------------------------------------------------------- 1 | import Grid from 'components/grid'; 2 | 3 | export default function Loading() { 4 | return ( 5 | 6 | {Array(12) 7 | .fill(0) 8 | .map((_, index) => { 9 | return ; 10 | })} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/[page]/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import OpengraphImage from 'components/opengraph-image'; 2 | import { getPage } from 'lib/shopify'; 3 | 4 | export const runtime = 'edge'; 5 | 6 | export default async function Image({ params }: { params: { page: string } }) { 7 | const page = await getPage(params.page); 8 | const title = page.seo?.title || page.title; 9 | 10 | return await OpengraphImage({ title }); 11 | } 12 | -------------------------------------------------------------------------------- /components/icons/arrow-left.tsx: -------------------------------------------------------------------------------- 1 | export default function ArrowLeftIcon({ className }: { className?: string }) { 2 | return ( 3 | 10 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/icons/search.tsx: -------------------------------------------------------------------------------- 1 | export default function SearchIcon({ className }: { className?: string }) { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/search/[collection]/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import OpengraphImage from 'components/opengraph-image'; 2 | import { getCollection } from 'lib/shopify'; 3 | 4 | export const runtime = 'edge'; 5 | 6 | export default async function Image({ params }: { params: { collection: string } }) { 7 | const collection = await getCollection(params.collection); 8 | const title = collection?.seo?.title || collection?.title; 9 | 10 | return await OpengraphImage({ title }); 11 | } 12 | -------------------------------------------------------------------------------- /components/icons/minus.tsx: -------------------------------------------------------------------------------- 1 | export default function MinusIcon({ className }: { className?: string }) { 2 | return ( 3 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/[page]/layout.tsx: -------------------------------------------------------------------------------- 1 | import Footer from 'components/layout/footer'; 2 | import { Suspense } from 'react'; 3 | 4 | export default function Layout({ children }: { children: React.ReactNode }) { 5 | return ( 6 | 7 |
8 |
9 | {children} 10 |
11 |
12 |