├── .nvmrc ├── .prettierignore ├── lib ├── swell │ ├── queries │ │ ├── cart.graphql │ │ ├── menu.graphql │ │ ├── categories.graphql │ │ └── product.graphql │ ├── fragments │ │ ├── Menu.graphql │ │ ├── Category.graphql │ │ ├── Cart.graphql │ │ └── Product.graphql │ ├── mutations │ │ └── cart.graphql │ └── index.ts ├── type-guards.ts ├── utils.ts └── constants.ts ├── app ├── favicon.ico ├── opengraph-image.tsx ├── api │ └── revalidate │ │ └── route.ts ├── robots.ts ├── search │ ├── [collection] │ │ ├── opengraph-image.tsx │ │ └── page.tsx │ ├── loading.tsx │ ├── layout.tsx │ └── page.tsx ├── globals.css ├── page.tsx ├── error.tsx ├── sitemap.ts ├── layout.tsx └── product │ └── [handle] │ └── page.tsx ├── fonts └── Inter-Bold.ttf ├── postcss.config.js ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .env.example ├── prettier.config.js ├── .vscode ├── settings.json └── launch.json ├── components ├── cart │ ├── index.tsx │ ├── close-cart.tsx │ ├── open-cart.tsx │ ├── delete-item-button.tsx │ ├── actions.ts │ ├── edit-item-quantity-button.tsx │ ├── add-to-cart.tsx │ └── modal.tsx ├── grid │ ├── index.tsx │ ├── tile.tsx │ └── three-items.tsx ├── loading-dots.tsx ├── icons │ └── logo.tsx ├── price.tsx ├── logo-square.tsx ├── prose.tsx ├── layout │ ├── product-grid-items.tsx │ ├── search │ │ ├── filter │ │ │ ├── index.tsx │ │ │ ├── dropdown.tsx │ │ │ └── item.tsx │ │ └── collections.tsx │ ├── footer-menu.tsx │ ├── navbar │ │ ├── search.tsx │ │ ├── index.tsx │ │ └── mobile-menu.tsx │ └── footer.tsx ├── label.tsx ├── opengraph-image.tsx ├── product │ ├── product-description.tsx │ ├── gallery.tsx │ └── variant-selector.tsx └── carousel.tsx ├── .eslintrc.js ├── next.config.js ├── .gitignore ├── .graphqlrc.yml ├── tsconfig.json ├── license.md ├── tailwind.config.js ├── package.json └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .vercel 2 | .next 3 | pnpm-lock.yaml 4 | -------------------------------------------------------------------------------- /lib/swell/queries/cart.graphql: -------------------------------------------------------------------------------- 1 | query getCart { 2 | cart { 3 | ...Cart 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/verswell-commerce/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/swellstores/verswell-commerce/HEAD/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /lib/swell/fragments/Menu.graphql: -------------------------------------------------------------------------------- 1 | fragment Menu on SwellSettingsMenusSection { 2 | id 3 | name 4 | items 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {} 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /lib/swell/queries/menu.graphql: -------------------------------------------------------------------------------- 1 | query getMenus { 2 | menuSettings { 3 | sections { 4 | ...Menu 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /lib/swell/fragments/Category.graphql: -------------------------------------------------------------------------------- 1 | fragment Category on SwellCategory { 2 | name 3 | slug 4 | metaDescription 5 | metaKeywords 6 | description 7 | } 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | COMPANY_NAME="Vercel Inc." 2 | TWITTER_CREATOR="@vercel" 3 | TWITTER_SITE="https://nextjs.org/commerce" 4 | SITE_NAME="Next.js Commerce" 5 | 6 | SWELL_PUBLIC_KEY= 7 | SWELL_STORE_ID= 8 | SWELL_REVALIDATION_SECRET= 9 | -------------------------------------------------------------------------------- /lib/swell/queries/categories.graphql: -------------------------------------------------------------------------------- 1 | query getCategories { 2 | categories { 3 | results { 4 | ...Category 5 | } 6 | } 7 | } 8 | 9 | query getGategory($slug: String!) { 10 | categoryBySlug(slug: $slug) { 11 | ...Category 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | singleQuote: true, 4 | arrowParens: 'always', 5 | trailingComma: 'none', 6 | printWidth: 100, 7 | tabWidth: 2, 8 | plugins: ['prettier-plugin-tailwindcss'] 9 | }; 10 | -------------------------------------------------------------------------------- /app/api/revalidate/route.ts: -------------------------------------------------------------------------------- 1 | import { revalidate } from 'lib/swell'; 2 | import { NextRequest, NextResponse } from 'next/server'; 3 | 4 | export const runtime = 'edge'; 5 | 6 | export async function POST(req: NextRequest): Promise { 7 | return revalidate(req); 8 | } 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /components/cart/index.tsx: -------------------------------------------------------------------------------- 1 | import { getCart } from 'lib/swell'; 2 | import { cookies } from 'next/headers'; 3 | import CartModal from './modal'; 4 | 5 | export default async function Cart() { 6 | const cartId = cookies().get('sessionToken')?.value; 7 | let cart; 8 | 9 | if (cartId) { 10 | cart = await getCart(cartId); 11 | } 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /app/search/[collection]/opengraph-image.tsx: -------------------------------------------------------------------------------- 1 | import OpengraphImage from 'components/opengraph-image'; 2 | import { getCategory } from 'lib/swell'; 3 | 4 | export const runtime = 'edge'; 5 | 6 | export default async function Image({ params }: { params: { collection: string } }) { 7 | const collection = await getCategory(params.collection); 8 | const title = collection?.name; 9 | 10 | return await OpengraphImage({ title }); 11 | } 12 | -------------------------------------------------------------------------------- /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 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/cart/close-cart.tsx: -------------------------------------------------------------------------------- 1 | import { XMarkIcon } from '@heroicons/react/24/outline'; 2 | import clsx from 'clsx'; 3 | 4 | export default function CloseCart({ className }: { className?: string }) { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /lib/swell/mutations/cart.graphql: -------------------------------------------------------------------------------- 1 | mutation addToCart($productId: ID!, $quantity: Int!, $options: [SwellCartItemOptionInput]) { 2 | addCartItem(input: { productId: $productId, quantity: $quantity, options: $options }) { 3 | ...Cart 4 | } 5 | } 6 | mutation editCartItem($itemId: String!, $quantity: Int!) { 7 | updateCartItem(itemId: $itemId, input: { quantity: $quantity }) { 8 | ...Cart 9 | } 10 | } 11 | 12 | mutation removeFromCart($itemId: String!) { 13 | deleteCartItem(itemId: $itemId) { 14 | ...Cart 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/swell/queries/product.graphql: -------------------------------------------------------------------------------- 1 | query getProduct($slug: String!) { 2 | productBySlug(slug: $slug) { 3 | ...Product 4 | } 5 | } 6 | 7 | query getProducts($sort: String, $query: String) { 8 | products(sort: $sort, search: $query) { 9 | results { 10 | ...Product 11 | } 12 | } 13 | } 14 | 15 | query getProductsByCategory($sort: String, $query: String, $category: String) { 16 | products(sort: $sort, search: $query, categories: [$category]) { 17 | results { 18 | ...Product 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['next', 'prettier'], 3 | plugins: ['unicorn'], 4 | rules: { 5 | 'no-unused-vars': [ 6 | 'error', 7 | { 8 | args: 'after-used', 9 | caughtErrors: 'none', 10 | ignoreRestSiblings: true, 11 | vars: 'all' 12 | } 13 | ], 14 | 'prefer-const': 'error', 15 | 'react-hooks/exhaustive-deps': 'error', 16 | 'unicorn/filename-case': [ 17 | 'error', 18 | { 19 | case: 'kebabCase' 20 | } 21 | ] 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /components/grid/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | function Grid(props: React.ComponentProps<'ul'>) { 4 | return ( 5 | 8 | ); 9 | } 10 | 11 | function GridItem(props: React.ComponentProps<'li'>) { 12 | return ( 13 |
  • 14 | {props.children} 15 |
  • 16 | ); 17 | } 18 | 19 | Grid.Item = GridItem; 20 | 21 | export default Grid; 22 | -------------------------------------------------------------------------------- /components/loading-dots.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | const dots = 'mx-[1px] inline-block h-1 w-1 animate-blink rounded-md'; 4 | 5 | const LoadingDots = ({ className }: { className: string }) => { 6 | return ( 7 | 8 | 9 | 10 | 11 | 12 | ); 13 | }; 14 | 15 | export default LoadingDots; 16 | -------------------------------------------------------------------------------- /components/icons/logo.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | export default function LogoIcon(props: React.ComponentProps<'svg'>) { 4 | return ( 5 | 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | eslint: { 4 | // Disabling on production builds because we're running checks on PRs via GitHub Actions. 5 | ignoreDuringBuilds: true 6 | }, 7 | images: { 8 | domains: ['cdn.schema.io', 'cdn.swell.store', 'media.istockphoto.com'], 9 | formats: ['image/avif', 'image/webp'], 10 | }, 11 | async redirects() { 12 | return [ 13 | { 14 | source: '/password', 15 | destination: '/', 16 | permanent: true 17 | } 18 | ]; 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /.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 | .playwright 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env* 31 | !.env.example 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @media (prefers-color-scheme: dark) { 6 | html { 7 | color-scheme: dark; 8 | } 9 | } 10 | 11 | @supports (font: -apple-system-body) and (-webkit-appearance: none) { 12 | img[loading='lazy'] { 13 | clip-path: inset(0.6px); 14 | } 15 | } 16 | 17 | a, 18 | input, 19 | button { 20 | @apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-400 focus-visible:ring-offset-2 focus-visible:ring-offset-neutral-50 dark:focus-visible:ring-neutral-600 dark:focus-visible:ring-offset-neutral-900; 21 | } 22 | -------------------------------------------------------------------------------- /lib/swell/fragments/Cart.graphql: -------------------------------------------------------------------------------- 1 | fragment Cart on SwellCart { 2 | checkoutUrl 3 | subTotal 4 | grandTotal 5 | currency 6 | taxes { 7 | amount 8 | } 9 | items { 10 | ...CartItem 11 | } 12 | } 13 | 14 | fragment CartItem on SwellCartItem { 15 | id 16 | quantity 17 | price 18 | discountTotal 19 | taxTotal 20 | variantId 21 | options { 22 | name 23 | value 24 | } 25 | variant { 26 | name 27 | } 28 | product { 29 | id 30 | name 31 | currency 32 | slug 33 | images { 34 | file { 35 | url 36 | width 37 | height 38 | } 39 | caption 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Carousel } from 'components/carousel'; 2 | import { ThreeItemGrid } from 'components/grid/three-items'; 3 | import Footer from 'components/layout/footer'; 4 | import { Suspense } from 'react'; 5 | 6 | export const runtime = 'edge'; 7 | 8 | export const metadata = { 9 | description: 'High-performance ecommerce store built with Next.js, Vercel, and Swell.', 10 | openGraph: { 11 | type: 'website' 12 | } 13 | }; 14 | 15 | export default async function HomePage() { 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 |