├── .nvmrc ├── .prettierignore ├── app ├── favicon.ico ├── opengraph-image.tsx ├── api │ └── revalidate │ │ └── route.ts ├── robots.ts ├── [page] │ ├── opengraph-image.tsx │ ├── layout.tsx │ └── page.tsx ├── search │ ├── loading.tsx │ ├── [collection] │ │ ├── opengraph-image.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── page.tsx ├── globals.css ├── page.tsx ├── error.tsx ├── layout.tsx ├── sitemap.ts └── product │ └── [handle] │ └── page.tsx ├── fonts └── Inter-Bold.ttf ├── postcss.config.js ├── .github ├── dependabot.yml └── workflows │ └── test.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 ├── type-guards.ts ├── constants.ts └── utils.ts ├── prettier.config.js ├── .env.example ├── .vscode ├── settings.json └── launch.json ├── components ├── cart │ ├── index.tsx │ ├── close-cart.tsx │ ├── open-cart.tsx │ ├── delete-item-button.tsx │ ├── edit-item-quantity-button.tsx │ ├── actions.ts │ ├── 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 ├── .gitignore ├── next.config.js ├── 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 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osseonews/commerce/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osseonews/commerce/HEAD/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/shopify'; 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 | -------------------------------------------------------------------------------- /.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 | SHOPIFY_REVALIDATION_SECRET="" 6 | SHOPIFY_STOREFRONT_ACCESS_TOKEN="" 7 | SHOPIFY_STORE_DOMAIN="[your-shopify-store-subdomain].myshopify.com" 8 | -------------------------------------------------------------------------------- /.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/shopify'; 2 | import { cookies } from 'next/headers'; 3 | import CartModal from './modal'; 4 | 5 | export default async function Cart() { 6 | const cartId = cookies().get('cartId')?.value; 7 | let cart; 8 | 9 | if (cartId) { 10 | cart = await getCart(cartId); 11 | } 12 | 13 | return ; 14 | } 15 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 |