├── apps ├── .gitkeep ├── storefront │ ├── app │ │ ├── lib │ │ │ ├── sanity │ │ │ │ └── index.ts │ │ │ └── theme.ts │ │ ├── queries │ │ │ ├── sanity │ │ │ │ ├── fragments │ │ │ │ │ ├── modules │ │ │ │ │ │ ├── instagram.ts │ │ │ │ │ │ ├── collection.ts │ │ │ │ │ │ ├── products.ts │ │ │ │ │ │ ├── product.ts │ │ │ │ │ │ ├── images.ts │ │ │ │ │ │ ├── accordion.ts │ │ │ │ │ │ ├── grid.ts │ │ │ │ │ │ ├── callout.ts │ │ │ │ │ │ ├── taggedProducts.ts │ │ │ │ │ │ ├── callToAction.ts │ │ │ │ │ │ └── image.ts │ │ │ │ │ ├── colorTheme.ts │ │ │ │ │ ├── linkExternal.ts │ │ │ │ │ ├── productWithVariant.ts │ │ │ │ │ ├── pages │ │ │ │ │ │ ├── notFound.ts │ │ │ │ │ │ ├── home.ts │ │ │ │ │ │ ├── page.ts │ │ │ │ │ │ ├── collection.ts │ │ │ │ │ │ ├── person.ts │ │ │ │ │ │ └── product.ts │ │ │ │ │ ├── productHotspot.ts │ │ │ │ │ ├── sharedText.ts │ │ │ │ │ ├── image.ts │ │ │ │ │ ├── seo.ts │ │ │ │ │ ├── seoShopify.ts │ │ │ │ │ ├── collectionGroup.ts │ │ │ │ │ ├── imageWithProductHotspots.ts │ │ │ │ │ ├── collection.ts │ │ │ │ │ ├── productWithVariantFields.ts │ │ │ │ │ ├── customProductOptions.ts │ │ │ │ │ ├── heroes │ │ │ │ │ │ ├── page.ts │ │ │ │ │ │ ├── collection.ts │ │ │ │ │ │ └── home.ts │ │ │ │ │ ├── material.ts │ │ │ │ │ ├── creator.ts │ │ │ │ │ ├── materialUpsells.ts │ │ │ │ │ ├── linkInternal.ts │ │ │ │ │ ├── portableText │ │ │ │ │ │ ├── markDefs.ts │ │ │ │ │ │ └── portableText.ts │ │ │ │ │ ├── links.ts │ │ │ │ │ ├── productFaqs.ts │ │ │ │ │ ├── productGuide.ts │ │ │ │ │ └── modules.ts │ │ │ │ ├── home.ts │ │ │ │ ├── person.ts │ │ │ │ ├── guide.ts │ │ │ │ ├── product.ts │ │ │ │ ├── collection.ts │ │ │ │ ├── page.ts │ │ │ │ └── layout.ts │ │ │ └── shopify │ │ │ │ └── collection.ts │ │ ├── routes │ │ │ ├── _store.$.tsx │ │ │ ├── _store.($lang).api.collections.$handle.tsx │ │ │ ├── [robots.txt].tsx │ │ │ ├── _store.($lang).account.logout.ts │ │ │ ├── _store.($lang).$shopid.orders.$token.authenticate.tsx │ │ │ ├── _store.($lang).api.fetchgids.tsx │ │ │ └── _store.tsx │ │ ├── styles │ │ │ ├── studio.css │ │ │ └── tailwind.css │ │ ├── entry.client.tsx │ │ ├── components │ │ │ ├── account │ │ │ │ ├── FormCardWrapper.tsx │ │ │ │ ├── OrderHistory.tsx │ │ │ │ ├── FormFieldText.tsx │ │ │ │ ├── FormFieldCheckbox.tsx │ │ │ │ └── Modal.tsx │ │ │ ├── global │ │ │ │ ├── PreviewLoading.tsx │ │ │ │ ├── collectionGroup │ │ │ │ │ └── CollectionGroup.tsx │ │ │ │ ├── Label.tsx │ │ │ │ ├── Skeleton.tsx │ │ │ │ ├── Header.tsx │ │ │ │ ├── Layout.tsx │ │ │ │ ├── GenericError.tsx │ │ │ │ ├── HeaderBackground.tsx │ │ │ │ └── Navigation.tsx │ │ │ ├── icons │ │ │ │ ├── ChevronDown.tsx │ │ │ │ ├── MinusCircle.tsx │ │ │ │ ├── Minus.tsx │ │ │ │ ├── User.tsx │ │ │ │ ├── PlusCircle.tsx │ │ │ │ ├── Menu.tsx │ │ │ │ ├── Plus.tsx │ │ │ │ ├── Close.tsx │ │ │ │ ├── ArrowRight.tsx │ │ │ │ ├── CreditCard.tsx │ │ │ │ ├── Remove.tsx │ │ │ │ ├── Spinner.tsx │ │ │ │ ├── Radio.tsx │ │ │ │ └── Logo.tsx │ │ │ ├── elements │ │ │ │ ├── CircleButton.tsx │ │ │ │ ├── CircleOutlineButton.tsx │ │ │ │ ├── Badge.tsx │ │ │ │ ├── LinkButton.tsx │ │ │ │ ├── Link.tsx │ │ │ │ └── Tooltip.tsx │ │ │ ├── portableText │ │ │ │ ├── blocks │ │ │ │ │ ├── Instagram.tsx │ │ │ │ │ ├── Callout.tsx │ │ │ │ │ ├── Products.tsx │ │ │ │ │ ├── Block.tsx │ │ │ │ │ ├── TaggedProducts.tsx │ │ │ │ │ ├── Accordion.tsx │ │ │ │ │ ├── Images.tsx │ │ │ │ │ └── Grid.tsx │ │ │ │ └── annotations │ │ │ │ │ ├── LinkEmail.tsx │ │ │ │ │ ├── LinkInternal.tsx │ │ │ │ │ ├── LinkExternal.tsx │ │ │ │ │ └── Product.tsx │ │ │ ├── modules │ │ │ │ ├── Instagram.tsx │ │ │ │ ├── Callout.tsx │ │ │ │ ├── Module.tsx │ │ │ │ └── Product.tsx │ │ │ ├── sanity │ │ │ │ └── SanityStudio.client.tsx │ │ │ ├── cart │ │ │ │ └── CartToggle.tsx │ │ │ ├── heroes │ │ │ │ ├── Home.tsx │ │ │ │ ├── HeroContent.tsx │ │ │ │ └── Page.tsx │ │ │ ├── preview │ │ │ │ └── PreviewBanner.tsx │ │ │ ├── product │ │ │ │ ├── ProductHero.tsx │ │ │ │ ├── RelatedProducts.tsx │ │ │ │ ├── buttons │ │ │ │ │ └── BuyNowButton.tsx │ │ │ │ ├── Tag.tsx │ │ │ │ ├── Magazine.tsx │ │ │ │ ├── Guide.tsx │ │ │ │ └── Hotspot.tsx │ │ │ └── media │ │ │ │ └── ImageWithProductHotspots.tsx │ │ ├── hooks │ │ │ ├── useCartFetchers.tsx │ │ │ ├── usePageAnalytics.tsx │ │ │ └── useAnalytics.tsx │ │ └── data │ │ │ ├── cache.ts │ │ │ └── countries.ts │ ├── .eslintignore │ ├── .graphqlrc.yml │ ├── public │ │ ├── favicon.ico │ │ └── favicon.png │ ├── prettier.config.js │ ├── .editorconfig │ ├── server.ts │ ├── postcss.config.js │ ├── vercel.json │ ├── .eslintrc.js │ ├── tsconfig.json │ ├── remix.config.js │ ├── .env.template │ ├── server.vercel.ts │ └── remix.env.d.ts └── studio │ ├── .prettierrc │ ├── vercel.json │ ├── .eslintrc │ ├── sanity.cli.ts │ ├── env.d.ts │ ├── sanity.config.ts │ ├── tsconfig.json │ ├── README.md │ ├── .env.example │ └── package.json ├── .nvmrc ├── packages ├── .gitkeep └── sanity │ ├── .prettierrc │ ├── .eslintrc │ ├── tsconfig.json │ ├── src │ ├── schema │ │ ├── documents │ │ │ ├── guide.ts │ │ │ ├── person.tsx │ │ │ ├── material.tsx │ │ │ └── colorTheme.tsx │ │ ├── objects │ │ │ ├── placeholderString.ts │ │ │ ├── proxyString.ts │ │ │ ├── creator.ts │ │ │ ├── productOption.tsx │ │ │ ├── hero │ │ │ │ ├── page.tsx │ │ │ │ ├── collection.tsx │ │ │ │ └── home.tsx │ │ │ ├── seo │ │ │ │ ├── home.tsx │ │ │ │ ├── page.tsx │ │ │ │ └── shopify.tsx │ │ │ ├── module │ │ │ │ ├── instagram.ts │ │ │ │ ├── callout.ts │ │ │ │ ├── product.tsx │ │ │ │ ├── products.tsx │ │ │ │ ├── collection.tsx │ │ │ │ └── taggedProducts.tsx │ │ │ ├── shopifyCollectionRule.tsx │ │ │ ├── faqs.ts │ │ │ ├── label.ts │ │ │ ├── linkExternal.ts │ │ │ ├── imageWithProductHotspots.ts │ │ │ └── linkInternal.ts │ │ ├── singletons │ │ │ ├── sharedText.ts │ │ │ └── home.ts │ │ ├── annotations │ │ │ ├── linkEmail.tsx │ │ │ ├── linkInternal.tsx │ │ │ └── linkExternal.tsx │ │ └── blocks │ │ │ └── simpleBlockContent.tsx │ ├── global.d.ts │ ├── desk │ │ ├── collections.ts │ │ ├── colorThemes.ts │ │ ├── preview.ts │ │ ├── home.tsx │ │ └── settings.tsx │ ├── utils │ │ ├── defineStructure.ts │ │ ├── blocksToText.ts │ │ ├── shopifyUrls.ts │ │ ├── getPriceRange.ts │ │ └── validateSlug.ts │ ├── plugins │ │ └── customDocumentActions │ │ │ ├── types.ts │ │ │ ├── shopifyLink.ts │ │ │ └── index.ts │ ├── components │ │ ├── inputs │ │ │ ├── PlaceholderString.tsx │ │ │ ├── CollectionHidden.tsx │ │ │ ├── ProductVariantHidden.tsx │ │ │ ├── ShopifyProductTagList.tsx │ │ │ └── ProxyString.tsx │ │ ├── studio │ │ │ ├── Navbar.tsx │ │ │ └── Logo.tsx │ │ ├── media │ │ │ ├── TranslatedDoc.tsx │ │ │ └── ColorTheme.tsx │ │ └── hotspots │ │ │ └── ProductTooltip.tsx │ └── constants.ts │ ├── tsconfig.settings.json │ └── package.json ├── commitlint.config.js ├── .husky ├── commit-msg └── pre-commit ├── .github ├── renovate.json └── workflows │ ├── ci.yml │ └── oxygen-deployment-1000007465.yml ├── .vscode └── extensions.json ├── .npmrc ├── turbo.json ├── package.json ├── LICENSE └── .gitignore /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/hydrogen -------------------------------------------------------------------------------- /packages/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/storefront/app/lib/sanity/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | -------------------------------------------------------------------------------- /apps/storefront/.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | bin 4 | *.d.ts 5 | dist 6 | -------------------------------------------------------------------------------- /apps/storefront/.graphqlrc.yml: -------------------------------------------------------------------------------- 1 | schema: node_modules/@shopify/hydrogen-react/storefront.schema.json 2 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | }; 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /apps/storefront/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/demo-ecommerce/HEAD/apps/storefront/public/favicon.ico -------------------------------------------------------------------------------- /apps/storefront/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sanity-io/demo-ecommerce/HEAD/apps/storefront/public/favicon.png -------------------------------------------------------------------------------- /apps/studio/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /packages/sanity/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "printWidth": 100, 4 | "bracketSpacing": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/instagram.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | export const MODULE_INSTAGRAM = groq` 4 | url 5 | `; 6 | -------------------------------------------------------------------------------- /apps/storefront/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require("@shopify/prettier-config"), 4 | require("prettier-plugin-tailwindcss"), 5 | ], 6 | }; 7 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | # Lint any workspace that changed in the last commit 5 | # npm run lint typecheck -- --filter=[HEAD^1] -------------------------------------------------------------------------------- /apps/storefront/.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 -------------------------------------------------------------------------------- /apps/storefront/server.ts: -------------------------------------------------------------------------------- 1 | import { handler } from "./handler"; 2 | 3 | /** 4 | * Export a fetch handler in module format. 5 | */ 6 | export default { 7 | fetch: handler, 8 | }; 9 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/colorTheme.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | export const COLOR_THEME = groq` 4 | 'background': background.hex, 5 | 'text': text.hex, 6 | `; 7 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "baseBranches": ["main"], 4 | "extends": [ 5 | "local>sanity-io/renovate-config" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/storefront/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "tailwindcss/nesting": {}, // must come before tailwindcss 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /apps/studio/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "framework": "sanity", 4 | "installCommand": "npm ci --prefix=../..", 5 | "ignoreCommand": "npx turbo-ignore" 6 | } 7 | -------------------------------------------------------------------------------- /apps/storefront/app/routes/_store.$.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "~/lib/utils"; 2 | 3 | export async function loader() { 4 | throw notFound(); 5 | } 6 | export default function Component() { 7 | return null; 8 | } 9 | -------------------------------------------------------------------------------- /apps/storefront/vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://openapi.vercel.sh/vercel.json", 3 | "framework": "hydrogen", 4 | "installCommand": "npm ci --prefix=../..", 5 | "ignoreCommand": "npx turbo-ignore" 6 | } 7 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/linkExternal.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | export const LINK_EXTERNAL = groq` 4 | _key, 5 | _type, 6 | newWindow, 7 | title, 8 | url, 9 | `; 10 | -------------------------------------------------------------------------------- /apps/studio/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/eslint-config-studio", 3 | "plugins": ["simple-import-sort"], 4 | "rules": { 5 | "simple-import-sort/imports": "warn", 6 | "simple-import-sort/exports": "warn" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/sanity/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@sanity/eslint-config-studio", 3 | "plugins": ["simple-import-sort"], 4 | "rules": { 5 | "simple-import-sort/imports": "warn", 6 | "simple-import-sort/exports": "warn" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "graphql.vscode-graphql", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode", 6 | "bradlc.vscode-tailwindcss", 7 | "sanity-io.vscode-sanity" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /apps/storefront/app/styles/studio.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | #sanity { 6 | height: 100vh; 7 | max-height: 100dvh; 8 | overscroll-behavior: none; 9 | -webkit-font-smoothing: antialiased; 10 | overflow: auto; 11 | } 12 | -------------------------------------------------------------------------------- /apps/studio/sanity.cli.ts: -------------------------------------------------------------------------------- 1 | import {defineCliConfig} from 'sanity/cli' 2 | 3 | export default defineCliConfig({ 4 | api: { 5 | projectId: process.env.SANITY_STUDIO_PROJECT_ID!, 6 | dataset: process.env.SANITY_STUDIO_DATASET!, 7 | }, 8 | }) 9 | -------------------------------------------------------------------------------- /packages/sanity/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.settings", 3 | "include": ["./src/**/*.d.ts", "./src/**/*.ts", "./src/**/*.tsx"], 4 | "compilerOptions": { 5 | "rootDir": ".", 6 | "jsx": "react-jsx", 7 | "noEmit": true, 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/collection.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { COLLECTION } from "../collection"; 4 | 5 | export const MODULE_COLLECTION = groq` 6 | collection->{ 7 | ${COLLECTION} 8 | }, 9 | showBackground 10 | `; 11 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/products.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { MODULE_PRODUCT } from "./product"; 4 | 5 | export const MODULE_PRODUCTS = groq` 6 | layout, 7 | modules[] { 8 | _key, 9 | ${MODULE_PRODUCT} 10 | } 11 | `; 12 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/product.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PRODUCT_WITH_VARIANT } from "../productWithVariant"; 4 | 5 | export const MODULE_PRODUCT = groq` 6 | productWithVariant { 7 | ...${PRODUCT_WITH_VARIANT} 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/productWithVariant.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PRODUCT_WITH_VARIANT_FIELDS } from "./productWithVariantFields"; 4 | 5 | export const PRODUCT_WITH_VARIANT = groq` 6 | product->{ 7 | ${PRODUCT_WITH_VARIANT_FIELDS} 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/home.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { HOME_PAGE } from "./fragments/pages/home"; 4 | 5 | export const HOME_PAGE_QUERY = groq` 6 | *[_type == 'home' && _id == 'home-' + $language] | order(_updatedAt desc) [0]{ 7 | ${HOME_PAGE} 8 | } 9 | `; 10 | -------------------------------------------------------------------------------- /packages/sanity/src/schema/documents/guide.ts: -------------------------------------------------------------------------------- 1 | import {EarthGlobeIcon} from '@sanity/icons' 2 | import {defineType} from 'sanity' 3 | 4 | import page from './page' 5 | 6 | export default defineType({ 7 | ...page, 8 | name: 'guide', 9 | title: 'Guide', 10 | icon: EarthGlobeIcon, 11 | }) 12 | -------------------------------------------------------------------------------- /packages/sanity/src/schema/objects/placeholderString.ts: -------------------------------------------------------------------------------- 1 | import PlaceholderStringInput from '../../components/inputs/PlaceholderString' 2 | 3 | export default { 4 | name: 'placeholderString', 5 | title: 'Title', 6 | type: 'string', 7 | components: { 8 | input: PlaceholderStringInput, 9 | }, 10 | } 11 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/pages/notFound.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { COLOR_THEME } from "../colorTheme"; 4 | 5 | export const NOT_FOUND_PAGE = groq` 6 | body, 7 | "collectionGid": collection->store.gid, 8 | colorTheme->{ 9 | ${COLOR_THEME} 10 | }, 11 | title 12 | `; 13 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/person.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PERSON_PAGE } from "./fragments/pages/person"; 4 | 5 | export const PERSON_QUERY = groq` 6 | *[ 7 | _type == 'person' 8 | && slug.current == $slug 9 | ] | order(_updatedAt desc) { 10 | ${PERSON_PAGE} 11 | }[0]`; 12 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/productHotspot.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PRODUCT_WITH_VARIANT } from "./productWithVariant"; 4 | 5 | export const PRODUCT_HOTSPOT = groq` 6 | _key, 7 | "product": productWithVariant { 8 | ...${PRODUCT_WITH_VARIANT} 9 | }, 10 | x, 11 | y 12 | `; 13 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/sharedText.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | export const SHARED_TEXT = groq` 4 | "sharedText": *[_type == 'sharedText'][0] { 5 | "deliveryAndReturns": coalesce(deliveryAndReturns[_key == $language][0].value, deliveryAndReturns[_key == $baseLanguage][0].value)[] 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/image.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | export const IMAGE = groq` 4 | ..., 5 | "altText": asset->altText, 6 | "blurDataURL": asset->metadata.lqip, 7 | 'height': asset->metadata.dimensions.height, 8 | 'url': asset->url, 9 | 'width': asset->metadata.dimensions.width, 10 | `; 11 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/seo.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { IMAGE } from "./image"; 4 | 5 | export const SEO = groq` 6 | "seo": { 7 | "description": seo.description, 8 | "image": seo.image { 9 | ${IMAGE} 10 | }, 11 | "title": coalesce(seo.title, title), 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/guide.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PAGE } from "./fragments/pages/page"; 4 | 5 | export const GUIDE_QUERY = groq` 6 | *[ 7 | _type == 'guide' 8 | && slug.current == $slug 9 | && language == $language 10 | ] | order(_updatedAt desc) [0]{ 11 | ${PAGE} 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/product.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PRODUCT_PAGE } from "./fragments/pages/product"; 4 | 5 | export const PRODUCT_PAGE_QUERY = groq` 6 | *[ 7 | _type == 'product' 8 | && store.slug.current == $slug 9 | ] | order(_updatedAt desc) [0]{ 10 | ${PRODUCT_PAGE} 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /packages/sanity/src/global.d.ts: -------------------------------------------------------------------------------- 1 | import type {ENVIRONMENT} from './constants' 2 | 3 | export declare global { 4 | interface Window { 5 | [ENVIRONMENT]: { 6 | preview: { 7 | domain?: string 8 | secret: string 9 | } 10 | shopify: { 11 | storeDomain: string 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/sanity/src/schema/objects/proxyString.ts: -------------------------------------------------------------------------------- 1 | import {defineField} from 'sanity' 2 | 3 | import ProxyStringInput from '../../components/inputs/ProxyString' 4 | 5 | export default defineField({ 6 | name: 'proxyString', 7 | title: 'Title', 8 | type: 'string', 9 | components: { 10 | input: ProxyStringInput, 11 | }, 12 | }) 13 | -------------------------------------------------------------------------------- /packages/sanity/tsconfig.settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "Bundler", 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 7 | "strict": true, 8 | "declaration": true, 9 | "skipLibCheck": true, 10 | "isolatedModules": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/storefront/app/entry.client.tsx: -------------------------------------------------------------------------------- 1 | import { RemixBrowser } from "@remix-run/react"; 2 | import { startTransition, StrictMode } from "react"; 3 | import { hydrateRoot } from "react-dom/client"; 4 | 5 | startTransition(() => { 6 | hydrateRoot( 7 | document, 8 | 9 | 10 | 11 | ); 12 | }); 13 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/seoShopify.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { IMAGE } from "./image"; 4 | 5 | export const SEO_SHOPIFY = groq` 6 | "seo": { 7 | "description": seo.description, 8 | "image": seo.image { 9 | ${IMAGE} 10 | }, 11 | "title": coalesce(seo.title, store.title), 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/collection.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { COLLECTION_PAGE } from "./fragments/pages/collection"; 4 | 5 | export const COLLECTION_PAGE_QUERY = groq` 6 | *[ 7 | _type == 'collection' 8 | && store.slug.current == $slug 9 | ] | order(_updatedAt desc) [0]{ 10 | ${COLLECTION_PAGE} 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /packages/sanity/src/desk/collections.ts: -------------------------------------------------------------------------------- 1 | import {ListItemBuilder} from 'sanity/structure' 2 | 3 | import defineStructure from '../utils/defineStructure' 4 | 5 | export default defineStructure((S) => 6 | S.listItem() 7 | .title('Collections') 8 | .schemaType('collection') 9 | .child(S.documentTypeList('collection')), 10 | ) 11 | -------------------------------------------------------------------------------- /packages/sanity/src/desk/colorThemes.ts: -------------------------------------------------------------------------------- 1 | import {ListItemBuilder} from 'sanity/structure' 2 | 3 | import defineStructure from '../utils/defineStructure' 4 | 5 | export default defineStructure((S) => 6 | S.listItem() 7 | .title('Color themes') 8 | .schemaType('colorTheme') 9 | .child(S.documentTypeList('colorTheme')), 10 | ) 11 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/images.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { MODULE_IMAGE } from "./image"; 4 | 5 | export const MODULE_IMAGES = groq` 6 | "fullWidth": select( 7 | count(modules) > 1 => true, 8 | fullWidth, 9 | ), 10 | layout, 11 | modules[] { 12 | _key, 13 | ${MODULE_IMAGE} 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /apps/studio/env.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | export interface ProcessEnv { 3 | SANITY_STUDIO_PROJECT_ID: string 4 | SANITY_STUDIO_DATASET?: string 5 | SANITY_STUDIO_API_VERSION?: string 6 | SANITY_STUDIO_PREVIEW_DOMAIN?: string 7 | SANITY_STUDIO_PREVIEW_SECRET: string 8 | SANITY_STUDIO_SHOPIFY_STORE_DOMAIN: string 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/pages/home.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { HERO_HOME } from "../heroes/home"; 4 | import { MODULES } from "../modules"; 5 | import { SEO } from "../seo"; 6 | 7 | export const HOME_PAGE = groq` 8 | hero { 9 | ${HERO_HOME} 10 | }, 11 | modules[] { 12 | ${MODULES} 13 | }, 14 | ${SEO} 15 | `; 16 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/collectionGroup.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { COLLECTION } from "./collection"; 4 | 5 | export const COLLECTION_GROUP = groq` 6 | _key, 7 | _type, 8 | collectionLinks[]->{ 9 | _key, 10 | ${COLLECTION} 11 | }, 12 | collectionProducts->{ 13 | ${COLLECTION} 14 | }, 15 | title, 16 | `; 17 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/accordion.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { MARK_DEFS } from "../portableText/markDefs"; 4 | 5 | export const MODULE_ACCORDION = groq` 6 | groups[] { 7 | _key, 8 | body[] { 9 | ..., 10 | markDefs[] { 11 | ${MARK_DEFS} 12 | } 13 | }, 14 | title, 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/imageWithProductHotspots.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { IMAGE } from "./image"; 4 | import { PRODUCT_HOTSPOT } from "./productHotspot"; 5 | 6 | export const IMAGE_WITH_PRODUCT_HOTSPOTS = groq` 7 | image { 8 | ${IMAGE} 9 | }, 10 | productHotspots[] { 11 | _key, 12 | ${PRODUCT_HOTSPOT} 13 | } 14 | `; 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | @shopify:registry=https://registry.npmjs.com 2 | progress=false 3 | 4 | # Adjust log level 5 | # https://docs.npmjs.com/cli/v8/using-npm/config#loglevel 6 | loglevel="warn" 7 | 8 | # Disable audit reports 9 | # https://docs.npmjs.com/cli/v8/using-npm/config#audit 10 | audit=false 11 | 12 | # Disable funding message 13 | # https://docs.npmjs.com/cli/v8/using-npm/config#fund 14 | fund=false -------------------------------------------------------------------------------- /packages/sanity/src/utils/defineStructure.ts: -------------------------------------------------------------------------------- 1 | import {ConfigContext} from 'sanity' 2 | import {StructureBuilder} from 'sanity/structure' 3 | 4 | /** 5 | * Helper for creating and typing composable desk structure parts. 6 | */ 7 | export default function defineStructure( 8 | factory: (S: StructureBuilder, context: ConfigContext) => StructureType, 9 | ) { 10 | return factory 11 | } 12 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/collection.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { COLOR_THEME } from "./colorTheme"; 4 | 5 | export const COLLECTION = groq` 6 | _id, 7 | _type, 8 | colorTheme->{ 9 | ${COLOR_THEME} 10 | }, 11 | "gid": store.gid, 12 | "slug": "/collections/" + store.slug.current, 13 | "title": store.title, 14 | "vector": vector.asset->url, 15 | `; 16 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/productWithVariantFields.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | export const PRODUCT_WITH_VARIANT_FIELDS = groq` 4 | _id, 5 | "_type": "productWithVariant", 6 | "available": !store.isDeleted && store.status == 'active', 7 | "gid": store.gid, 8 | "slug": store.slug.current, 9 | "variantGid": coalesce(^.variant->store.gid, store.variants[0]->store.gid) 10 | `; 11 | -------------------------------------------------------------------------------- /packages/sanity/src/plugins/customDocumentActions/types.ts: -------------------------------------------------------------------------------- 1 | import {type DocumentActionProps, type SanityDocument} from 'sanity' 2 | 3 | export type ShopifyDocument = SanityDocument & { 4 | store: { 5 | id: number 6 | productId: number 7 | isDeleted: boolean 8 | } 9 | } 10 | 11 | export interface ShopifyDocumentActionProps extends DocumentActionProps { 12 | published: ShopifyDocument 13 | draft: ShopifyDocument 14 | } 15 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/grid.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { IMAGE } from "../image"; 4 | import { MARK_DEFS } from "../portableText/markDefs"; 5 | 6 | export const MODULE_GRID = groq` 7 | items[] { 8 | _key, 9 | body[]{ 10 | ..., 11 | markDefs[] { 12 | ${MARK_DEFS} 13 | } 14 | }, 15 | image { 16 | ${IMAGE} 17 | }, 18 | title 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/callout.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { LINK_EXTERNAL } from "../linkExternal"; 4 | import { LINK_INTERNAL } from "../linkInternal"; 5 | 6 | export const MODULE_CALLOUT = groq` 7 | "link": links[0] { 8 | (_type == 'linkExternal') => { 9 | ${LINK_EXTERNAL} 10 | }, 11 | (_type == 'linkInternal') => { 12 | ${LINK_INTERNAL} 13 | }, 14 | }, 15 | text 16 | `; 17 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/customProductOptions.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | export const CUSTOM_PRODUCT_OPTIONS = groq` 4 | _key, 5 | _type, 6 | title, 7 | (_type == 'customProductOption.color') => { 8 | colors[] { 9 | "hex": color.hex, 10 | title, 11 | }, 12 | }, 13 | (_type == 'customProductOption.size') => { 14 | sizes[] { 15 | height, 16 | title, 17 | width 18 | }, 19 | }, 20 | `; 21 | -------------------------------------------------------------------------------- /apps/storefront/app/components/account/FormCardWrapper.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | type Props = { 4 | children?: ReactNode; 5 | title: string; 6 | }; 7 | 8 | export default function FormCardWrapper({ children, title }: Props) { 9 | return ( 10 |
11 |

{title}

12 | {children} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/page.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PAGE } from "./fragments/pages/page"; 4 | 5 | export const PAGE_QUERY = groq` 6 | coalesce( 7 | *[ 8 | _type == 'page' 9 | && slug.current == $slug 10 | && language == $language 11 | ][0], 12 | *[ 13 | _type == 'page' 14 | && slug.current == $slug 15 | && (language == $baseLanguage || !defined(language)) 16 | ][0] 17 | ) { 18 | ${PAGE} 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /apps/studio/sanity.config.ts: -------------------------------------------------------------------------------- 1 | import {defineSanityConfig} from '@demo-ecommerce/sanity' 2 | 3 | export default defineSanityConfig({ 4 | projectId: process.env.SANITY_STUDIO_PROJECT_ID, 5 | dataset: process.env.SANITY_STUDIO_DATASET ?? 'production', 6 | preview: { 7 | domain: process.env.SANITY_STUDIO_PREVIEW_DOMAIN, 8 | secret: process.env.SANITY_STUDIO_PREVIEW_SECRET, 9 | }, 10 | shopify: { 11 | storeDomain: process.env.SANITY_STUDIO_SHOPIFY_STORE_DOMAIN!, 12 | }, 13 | }) 14 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/modules/taggedProducts.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { PRODUCT_WITH_VARIANT_FIELDS } from "../productWithVariantFields"; 4 | 5 | export const MODULE_TAGGED_PRODUCTS = groq` 6 | tag, 7 | layout, 8 | number, 9 | "products": *[ 10 | _type == "product" && ^.tag in string::split(store.tags,',') 11 | ] | order(_createdAt desc) { 12 | "productWithVariant": { 13 | ${PRODUCT_WITH_VARIANT_FIELDS} 14 | } 15 | } [0..3] 16 | `; 17 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env.*local"], 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "lint": { 10 | "cache": false 11 | }, 12 | "format": { 13 | "cache": false 14 | }, 15 | "typecheck": { 16 | "cache": false 17 | }, 18 | "dev": { 19 | "cache": false, 20 | "persistent": true 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /apps/studio/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "ESNext", 10 | "moduleResolution": "Bundler", 11 | "isolatedModules": true, 12 | "jsx": "preserve", 13 | "incremental": true, 14 | }, 15 | "include": ["**/*.ts", "**/*.tsx", "**/*.d.ts"], 16 | "exclude": ["node_modules"], 17 | } 18 | -------------------------------------------------------------------------------- /apps/storefront/app/lib/theme.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "react"; 2 | 3 | export type SanityColorTheme = { 4 | background: string; 5 | text: string; 6 | }; 7 | 8 | const ColorThemeContext = createContext( 9 | null 10 | ); 11 | export const ColorTheme = ColorThemeContext.Provider; 12 | 13 | /** 14 | * Returns the applied color theme, comprising background and text colors 15 | */ 16 | export const useColorTheme = () => useContext(ColorThemeContext); 17 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/heroes/page.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { IMAGE_WITH_PRODUCT_HOTSPOTS } from "../imageWithProductHotspots"; 4 | import { PRODUCT_WITH_VARIANT } from "../productWithVariant"; 5 | 6 | export const HERO_PAGE = groq` 7 | content[0] { 8 | _type, 9 | (_type == 'imageWithProductHotspots') => { 10 | ${IMAGE_WITH_PRODUCT_HOTSPOTS} 11 | }, 12 | (_type == 'productWithVariant') => { 13 | ...${PRODUCT_WITH_VARIANT} 14 | }, 15 | }, 16 | title 17 | `; 18 | -------------------------------------------------------------------------------- /apps/storefront/app/components/global/PreviewLoading.tsx: -------------------------------------------------------------------------------- 1 | import SpinnerIcon from "../icons/Spinner"; 2 | 3 | export function PreviewLoading() { 4 | return ( 5 |
6 |
10 | 11 | Loading preview... 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/pages/page.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { COLOR_THEME } from "../colorTheme"; 4 | import { HERO_PAGE } from "../heroes/page"; 5 | import { PORTABLE_TEXT } from "../portableText/portableText"; 6 | import { SEO } from "../seo"; 7 | 8 | export const PAGE = groq` 9 | body[]{ 10 | ${PORTABLE_TEXT} 11 | }, 12 | colorTheme->{ 13 | ${COLOR_THEME} 14 | }, 15 | (showHero == true) => { 16 | hero { 17 | ${HERO_PAGE} 18 | }, 19 | }, 20 | ${SEO}, 21 | title, 22 | `; 23 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/material.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { MARK_DEFS } from "./portableText/markDefs"; 4 | 5 | export const MATERIAL = groq` 6 | _key, 7 | 'material': @->{ 8 | _id, 9 | "name": coalesce(name[_key == $language][0].value, name[_key == $baseLanguage][0].value), 10 | attributes, 11 | "story": coalesce(story[_key == $language][0].value, story[_key == $baseLanguage][0].value)[] { 12 | ..., 13 | markDefs[] { 14 | ${MARK_DEFS} 15 | } 16 | } 17 | } 18 | `; 19 | -------------------------------------------------------------------------------- /apps/studio/README.md: -------------------------------------------------------------------------------- 1 | # Sanity Clean Content Studio 2 | 3 | Congratulations, you have now installed the Sanity Content Studio, an open source real-time content editing environment connected to the Sanity backend. 4 | 5 | Now you can do the following things: 6 | 7 | - [Read “getting started” in the docs](https://www.sanity.io/docs/introduction/getting-started?utm_source=readme) 8 | - [Join the community Slack](https://slack.sanity.io/?utm_source=readme) 9 | - [Extend and build plugins](https://www.sanity.io/docs/content-studio/extending?utm_source=readme) 10 | -------------------------------------------------------------------------------- /apps/storefront/app/components/global/collectionGroup/CollectionGroup.tsx: -------------------------------------------------------------------------------- 1 | import CollectionGroupDialog from "~/components/global/collectionGroup/CollectionGroupDialog"; 2 | import type { SanityCollectionGroup } from "~/lib/sanity"; 3 | 4 | type Props = { 5 | collectionGroup: SanityCollectionGroup; 6 | }; 7 | 8 | export default function CollectionGroup({ collectionGroup }: Props) { 9 | return ( 10 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/creator.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { IMAGE } from "./image"; 4 | import { MARK_DEFS } from "./portableText/markDefs"; 5 | 6 | export const CREATOR = groq` 7 | _key, 8 | role, 9 | person->{ 10 | name, 11 | "slug": "/people/" + slug.current, 12 | image { 13 | ${IMAGE} 14 | }, 15 | "bio": coalesce(bio[_key == $language][0].value, bio[_key == $baseLanguage][0].value)[] { 16 | ..., 17 | markDefs[] { 18 | ${MARK_DEFS} 19 | } 20 | } 21 | } 22 | `; 23 | -------------------------------------------------------------------------------- /apps/storefront/app/queries/sanity/fragments/heroes/collection.ts: -------------------------------------------------------------------------------- 1 | import groq from "groq"; 2 | 3 | import { IMAGE_WITH_PRODUCT_HOTSPOTS } from "../imageWithProductHotspots"; 4 | import { PRODUCT_WITH_VARIANT } from "../productWithVariant"; 5 | 6 | export const HERO_COLLECTION = groq` 7 | content[0] { 8 | _type, 9 | (_type == 'imageWithProductHotspots') => { 10 | ${IMAGE_WITH_PRODUCT_HOTSPOTS} 11 | }, 12 | (_type == 'productWithVariant') => { 13 | ...${PRODUCT_WITH_VARIANT} 14 | }, 15 | }, 16 | description, 17 | title 18 | `; 19 | -------------------------------------------------------------------------------- /apps/storefront/app/components/icons/ChevronDown.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGAttributes } from "react"; 2 | 3 | export function ChevronDownIcon(props: SVGAttributes) { 4 | return ( 5 | 13 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /apps/storefront/app/components/icons/MinusCircle.tsx: -------------------------------------------------------------------------------- 1 | export default function MinusCircleIcon() { 2 | return ( 3 | 4 | 8 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /apps/storefront/app/components/icons/Minus.tsx: -------------------------------------------------------------------------------- 1 | export default function MinusIcon() { 2 | return ( 3 | 10 | 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: ['main'] 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-node@v4 19 | with: 20 | node-version: lts/* 21 | cache: npm 22 | - run: npm ci 23 | - run: npm run lint 24 | - run: npm run typecheck 25 | -------------------------------------------------------------------------------- /apps/storefront/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("@types/eslint").Linter.BaseConfig} 3 | */ 4 | module.exports = { 5 | extends: [ 6 | "@remix-run/eslint-config", 7 | "plugin:hydrogen/recommended", 8 | "plugin:hydrogen/typescript", 9 | ], 10 | plugins: ["simple-import-sort"], 11 | rules: { 12 | "simple-import-sort/imports": "warn", 13 | "simple-import-sort/exports": "warn", 14 | "@typescript-eslint/ban-ts-comment": "off", 15 | "@typescript-eslint/naming-convention": "off", 16 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off", 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /apps/storefront/app/hooks/useCartFetchers.tsx: -------------------------------------------------------------------------------- 1 | import { useFetchers } from "@remix-run/react"; 2 | import { CartForm } from "@shopify/hydrogen"; 3 | 4 | export function useCartFetchers(actionName: string) { 5 | const fetchers = useFetchers(); 6 | const cartFetchers = []; 7 | 8 | for (const fetcher of fetchers) { 9 | const formData = fetcher?.formData; 10 | if (formData) { 11 | const formInputs = CartForm.getFormInput(formData); 12 | if (formInputs.action === actionName) { 13 | cartFetchers.push(fetcher); 14 | } 15 | } 16 | } 17 | return cartFetchers; 18 | } 19 | -------------------------------------------------------------------------------- /apps/storefront/app/components/elements/CircleButton.tsx: -------------------------------------------------------------------------------- 1 | import clsx from "clsx"; 2 | import type { HTMLAttributes } from "react"; 3 | 4 | type Props = HTMLAttributes; 5 | 6 | export default function CircleButton(props: Props) { 7 | const { className, ...rest } = props; 8 | 9 | return ( 10 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /apps/storefront/app/components/product/ProductHero.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from "@shopify/hydrogen"; 2 | import type { 3 | Product, 4 | ProductVariant, 5 | } from "@shopify/hydrogen/storefront-api-types"; 6 | 7 | import ProductTile from "~/components/product/Tile"; 8 | import { useGid } from "~/lib/utils"; 9 | 10 | type Props = { 11 | gid: string; 12 | variantGid: string; 13 | }; 14 | 15 | export default function ProductHero({ gid, variantGid }: Props) { 16 | const storefrontProduct = useGid(gid); 17 | const firstVariant = 18 | useGid(variantGid) ?? 19 | storefrontProduct?.variants.nodes.find( 20 | (variant) => variant.id == variantGid 21 | ) ?? 22 | storefrontProduct?.variants.nodes[0]; 23 | 24 | if (!(storefrontProduct && firstVariant)) { 25 | return null; 26 | } 27 | 28 | return ( 29 | <> 30 | {firstVariant.image && ( 31 | 36 | )} 37 | 38 |
39 | 40 |
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /apps/storefront/app/components/global/Layout.tsx: -------------------------------------------------------------------------------- 1 | import { usePreviewContext } from "hydrogen-sanity"; 2 | 3 | import Footer from "~/components/global/Footer"; 4 | import Header from "~/components/global/Header"; 5 | import { PreviewBanner } from "~/components/preview/PreviewBanner"; 6 | 7 | import { Label } from "./Label"; 8 | 9 | type LayoutProps = { 10 | backgroundColor?: string; 11 | children: React.ReactNode; 12 | }; 13 | 14 | export function Layout({ backgroundColor, children }: LayoutProps) { 15 | const isPreview = Boolean(usePreviewContext()); 16 | 17 | return ( 18 | <> 19 |
20 | 24 | 26 |
27 | 28 |
32 |
33 | 34 |
35 |
{children}
36 |
37 |
38 | 39 |