├── .env.example ├── .npmrc ├── src ├── routes │ ├── +layout.gql │ ├── category │ │ └── [slug] │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ ├── +page.gql │ ├── product │ │ └── [slug] │ │ │ ├── +page.ts │ │ │ ├── +page.gql │ │ │ └── +page.svelte │ ├── search │ │ └── [searchTerm] │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ ├── +layout.ts │ ├── +page.svelte │ └── +layout.svelte ├── stores │ ├── cart.ts │ ├── filters.ts │ ├── locale.ts │ └── currencyCode.ts ├── app.css ├── lib │ ├── images │ │ ├── cube-logo-line-icon-nostroke.png │ │ └── abel-y-costa-716024-unsplash__preview.jpg │ ├── components │ │ ├── page-transition.svelte │ │ ├── icons │ │ │ ├── minus.svelte │ │ │ ├── plus.svelte │ │ │ ├── facebook.svelte │ │ │ ├── magnifying-glass.svelte │ │ │ ├── you-tube.svelte │ │ │ ├── shopping-cart.svelte │ │ │ ├── sad-face.svelte │ │ │ └── twitter.svelte │ │ ├── category-banner.svelte │ │ ├── product-card.svelte │ │ ├── search.svelte │ │ ├── hero.svelte │ │ ├── filters.svelte │ │ ├── footer.svelte │ │ ├── navbar.svelte │ │ └── cart.svelte │ ├── graphql │ │ ├── QUERY.GetCollections.gql │ │ └── QUERY.search-products.gql │ └── utils │ │ └── index.ts ├── hooks.server.ts ├── app.html ├── app.d.ts └── client.ts ├── static └── favicon.png ├── renovate.json ├── .vscode ├── extensions.json └── settings.json ├── .gitignore ├── .prettierignore ├── .graphqlrc.yaml ├── vite.config.ts ├── tailwind.config.cjs ├── .prettierrc ├── houdini.config.js ├── postcss.config.cjs ├── tsconfig.json ├── .eslintrc.cjs ├── svelte.config.js ├── README.md ├── package.json └── schema.graphql /.env.example: -------------------------------------------------------------------------------- 1 | VITE_GRAPHQL_ENDPOINT=https://readonlydemo.vendure.io/shop-api 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | node-linker=hoisted 3 | strict-peer-dependencies=false 4 | -------------------------------------------------------------------------------- /src/routes/+layout.gql: -------------------------------------------------------------------------------- 1 | query LayoutQuery { 2 | activeChannel { 3 | currencyCode 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-vendure-commerce/HEAD/static/favicon.png -------------------------------------------------------------------------------- /src/stores/cart.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | export const cartOpen = writable(false) 4 | -------------------------------------------------------------------------------- /src/stores/filters.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | export const filtersStore = writable([]) 4 | -------------------------------------------------------------------------------- /src/stores/locale.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | export const userLocale = writable(null) 4 | -------------------------------------------------------------------------------- /src/stores/currencyCode.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | 3 | export const currencyCode = writable(null) 4 | -------------------------------------------------------------------------------- /src/app.css: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in PostCSS syntax */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/images/cube-logo-line-icon-nostroke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-vendure-commerce/HEAD/src/lib/images/cube-logo-line-icon-nostroke.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "bradlc.vscode-tailwindcss", 4 | "svelte.svelte-vscode", 5 | "GraphQL.vscode-graphql" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /src/lib/images/abel-y-costa-716024-unsplash__preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spences10/sveltekit-vendure-commerce/HEAD/src/lib/images/abel-y-costa-716024-unsplash__preview.jpg -------------------------------------------------------------------------------- /src/routes/category/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | 3 | export const load: PageLoad = async event => { 4 | const { slug } = event.params 5 | 6 | return { slug } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .pnpm-debug.log 10 | .vercel_build_output 11 | .vercel 12 | 13 | $houdini 14 | $kitql -------------------------------------------------------------------------------- /src/routes/+page.gql: -------------------------------------------------------------------------------- 1 | query GetTopSellers { 2 | search( 3 | input: { take: 8, groupByProduct: true, sort: { price: ASC } } 4 | ) { 5 | items { 6 | ...productCartDetail 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock -------------------------------------------------------------------------------- /.graphqlrc.yaml: -------------------------------------------------------------------------------- 1 | projects: 2 | default: 3 | schema: 4 | - ./schema.graphql 5 | - ./$houdini/graphql/schema.graphql 6 | documents: 7 | - '**/*.gql' 8 | - '**/*.svelte' 9 | - ./$houdini/graphql/documents.gql 10 | -------------------------------------------------------------------------------- /src/lib/components/page-transition.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | {#key refresh} 7 |
8 | 9 |
10 | {/key} 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "git.enableSmartCommit": true, 3 | "git.postCommitCommand": "sync", 4 | "css.validate": false, 5 | "cSpell.words": [ 6 | "Color", 7 | "daisyui", 8 | "kitql", 9 | "nostroke", 10 | "sveltejs", 11 | "VITE" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import houdini from 'houdini/vite'; 3 | import type { UserConfig } from "vite"; 4 | 5 | const config: UserConfig = { 6 | plugins: [houdini(), sveltekit()], 7 | } 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | content: ['./src/**/*.{html,js,svelte,ts}'], 3 | 4 | theme: { 5 | extend: {}, 6 | }, 7 | 8 | plugins: [require('daisyui')], 9 | 10 | daisyui: { 11 | themes: ['garden'], 12 | }, 13 | } 14 | 15 | module.exports = config 16 | -------------------------------------------------------------------------------- /src/routes/product/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | // file will be removed in the next version :) 2 | 3 | import type { GetProductDetailVariables as V } from './$houdini' 4 | 5 | export const _GetProductDetailVariables: V = async event => { 6 | return { 7 | slug: event.params.slug, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/lib/components/icons/minus.svelte: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/routes/search/[searchTerm]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | 3 | export const load: PageLoad = async event => { 4 | // await GQL_SearchProducts.fetch({ 5 | // event, 6 | // variables: { input: {} }, 7 | // }) 8 | const { searchTerm } = event.params 9 | 10 | return { searchTerm } 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import { dev } from '$app/environment' 2 | import { handleGraphiql } from '@kitql/all-in' 3 | import { sequence } from '@sveltejs/kit/hooks' 4 | 5 | export const handle = sequence( 6 | // Add graphiql 7 | handleGraphiql({ 8 | endpoint: 'https://readonlydemo.vendure.io/shop-api', 9 | enabled: dev, 10 | }) 11 | ) 12 | -------------------------------------------------------------------------------- /src/lib/components/icons/plus.svelte: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/icons/facebook.svelte: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "semi": false, 5 | "trailingComma": "es5", 6 | "printWidth": 70, 7 | "arrowParens": "avoid", 8 | "proseWrap": "always", 9 | "plugins": ["prettier-plugin-svelte"], 10 | "pluginSearchDirs": ["."], 11 | "overrides": [ 12 | { "files": "*.svelte", "options": { "parser": "svelte" } } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/lib/components/icons/magnifying-glass.svelte: -------------------------------------------------------------------------------- 1 | 7 | 13 | 14 | -------------------------------------------------------------------------------- /houdini.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('houdini').ConfigFile} */ 2 | const config = { 3 | apiUrl: 'https://readonlydemo.vendure.io/shop-api', 4 | plugins: { 5 | 'houdini-svelte': { 6 | client: './src/client', 7 | }, 8 | }, 9 | scalars: { 10 | DateTime: { 11 | type: 'string', 12 | }, 13 | }, 14 | defaultFragmentMasking: true, 15 | } 16 | 17 | export default config 18 | -------------------------------------------------------------------------------- /src/lib/graphql/QUERY.GetCollections.gql: -------------------------------------------------------------------------------- 1 | query GetCollections($options: CollectionListOptions) { 2 | collections(options: $options) { 3 | items { 4 | id 5 | name 6 | slug 7 | parent { 8 | id 9 | slug 10 | name 11 | } 12 | featuredAsset { 13 | id 14 | preview 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss') 2 | const autoprefixer = require('autoprefixer') 3 | 4 | const config = { 5 | plugins: [ 6 | //Some plugins, like tailwindcss/nesting, need to run before Tailwind, 7 | tailwindcss(), 8 | //But others, like autoprefixer, need to run after, 9 | autoprefixer, 10 | ], 11 | } 12 | 13 | module.exports = config 14 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/icons/you-tube.svelte: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": false, 12 | "preserveValueImports": false, 13 | "rootDirs": [".", "./.svelte-kit/types", "./$houdini/types"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import { 2 | load_GetCollections, 3 | load_LayoutQuery, 4 | load_GetTopSellers, 5 | } from '$houdini' 6 | import '../app.css' 7 | import type { LayoutLoad } from './$types' 8 | 9 | export const load: LayoutLoad = async event => { 10 | return { 11 | key: event.url.pathname, 12 | ...(await load_GetTopSellers({ event })), 13 | ...(await load_LayoutQuery({ event })), 14 | ...(await load_GetCollections({ event })), 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/components/icons/shopping-cart.svelte: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // See https://kit.svelte.dev/docs/types#the-app-namespace 4 | // for information about these interfaces 5 | // declare namespace App { 6 | // interface Locals {} 7 | // // interface Platform {} 8 | // // interface Session {} 9 | // // interface Stuff {} 10 | // } 11 | 12 | declare namespace svelte.JSX { 13 | interface DOMAttributes { 14 | onclick_outside?: CompositionEventHandler 15 | } 16 | } 17 | 18 | interface ImportMetaEnv { 19 | readonly VITE_GRAPHQL_ENDPOINT: string 20 | } 21 | -------------------------------------------------------------------------------- /src/lib/components/icons/sad-face.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | 21 | 22 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'prettier', 8 | ], 9 | plugins: ['svelte3', '@typescript-eslint'], 10 | ignorePatterns: ['*.cjs'], 11 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], 12 | settings: { 13 | 'svelte3/typescript': () => require('typescript'), 14 | }, 15 | parserOptions: { 16 | sourceType: 'module', 17 | ecmaVersion: 2020, 18 | }, 19 | env: { 20 | browser: true, 21 | es2017: true, 22 | node: true, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-vercel' 2 | import preprocess from 'svelte-preprocess' 3 | import { resolve } from 'path' 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | // Consult https://github.com/sveltejs/svelte-preprocess 8 | // for more information about preprocessors 9 | preprocess: preprocess(), 10 | 11 | kit: { 12 | adapter: adapter(), 13 | alias: { 14 | $houdini: resolve('./$houdini'), 15 | $components: resolve('./src/lib/components'), 16 | $lib: resolve('./src/lib'), 17 | $stores: resolve('./src/stores'), 18 | }, 19 | }, 20 | } 21 | 22 | export default config 23 | -------------------------------------------------------------------------------- /src/lib/components/icons/twitter.svelte: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/graphql/QUERY.search-products.gql: -------------------------------------------------------------------------------- 1 | query SearchProducts($input: SearchInput!) { 2 | search(input: $input) { 3 | items { 4 | ...productCartDetail 5 | productId 6 | slug 7 | productName 8 | description 9 | priceWithTax { 10 | ... on PriceRange { 11 | min 12 | max 13 | } 14 | } 15 | productAsset { 16 | id 17 | preview 18 | focalPoint { 19 | x 20 | y 21 | } 22 | } 23 | } 24 | totalItems 25 | facetValues { 26 | count 27 | facetValue { 28 | id 29 | name 30 | facet { 31 | id 32 | name 33 | } 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vendure SvelteKit Storefront 2 | 3 | ## This project is maintained over at https://github.com/vendure-ecommerce/storefront-sveltekit-starter 4 | 5 | ## Tools 6 | 7 | - ✅ [Vendure](https://www.vendure.io/) 8 | - 🧡 [SvelteKit](https://kit.svelte.dev/) 9 | - ⚡ [KitQL](https://www.kitql.dev/) 10 | - 🎩 [Houdini](https://www.houdinigraphql.com/) 11 | - 🎨 [Tailwindcss](https://tailwindcss.com/) 12 | - 🖼️ [daisyUI](https://daisyui.com/) 13 | 14 | ## Development 15 | 16 | ```bash 17 | # 1/ clone the repo 18 | git clone git@github.com:spences10/sveltekit-vendure-commerce.git 19 | 20 | # 2/ install dependencies 21 | pnpm i 22 | 23 | # 3/ start your local developement 24 | pnpm dev 25 | 26 | # enjoy 🥳🥳🥳 27 | ``` 28 | 29 | ## Production 30 | 31 | ```bash 32 | # 1/ build your app 33 | pnpm build 34 | ``` 35 | -------------------------------------------------------------------------------- /src/lib/components/category-banner.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 | 21 | 22 |

Top Sellers

23 | 24 |
27 | {#each $GetTopSellers.data?.search?.items ?? [] as item} 28 | 29 | {/each} 30 |
31 | -------------------------------------------------------------------------------- /src/routes/product/[slug]/+page.gql: -------------------------------------------------------------------------------- 1 | query GetProductDetail($slug: String!) { 2 | product(slug: $slug) { 3 | id 4 | name 5 | description 6 | variants { 7 | # fragment Variant on ProductVariant { 8 | id 9 | name 10 | options { 11 | code 12 | name 13 | } 14 | price 15 | priceWithTax 16 | sku 17 | # } 18 | } 19 | featuredAsset { 20 | # fragment Asset on Asset { 21 | id 22 | width 23 | height 24 | name 25 | preview 26 | focalPoint { 27 | x 28 | y 29 | } 30 | # } 31 | } 32 | assets { 33 | # fragment Asset on Asset { 34 | id 35 | width 36 | height 37 | name 38 | preview 39 | focalPoint { 40 | x 41 | y 42 | } 43 | # } 44 | } 45 | collections { 46 | id 47 | slug 48 | breadcrumbs { 49 | id 50 | name 51 | slug 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/lib/components/product-card.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | 32 | {$frag?.productName} 37 | 38 |

{$frag?.productName}

39 |

40 | {formatCurrency($frag?.priceWithTax.max) || 0} 41 |

42 |
43 | -------------------------------------------------------------------------------- /src/lib/utils/index.ts: -------------------------------------------------------------------------------- 1 | import { currencyCode } from '$stores/currencyCode' 2 | import { userLocale } from '$stores/locale' 3 | import { get } from 'svelte/store' 4 | 5 | export const formatCurrency = (value: number) => { 6 | const majorUnits = value / 100 7 | 8 | const locale = get(userLocale) ?? 'en-US' 9 | const currency = get(currencyCode) ?? 'USD' 10 | 11 | return new Intl.NumberFormat(locale, { 12 | style: 'currency', 13 | currency: currency, 14 | }).format(majorUnits) 15 | } 16 | 17 | // https://svelte.dev/repl/0ace7a508bd843b798ae599940a91783?version=3.16.7 18 | /** Dispatch event on click outside of node */ 19 | export const clickOutside = (node: any) => { 20 | const handleClick = (event: any) => { 21 | if ( 22 | node && 23 | !node.contains(event.target) && 24 | !event.defaultPrevented 25 | ) { 26 | node.dispatchEvent(new CustomEvent('click_outside', node)) 27 | } 28 | } 29 | 30 | document.addEventListener('click', handleClick, true) 31 | 32 | return { 33 | destroy() { 34 | document.removeEventListener('click', handleClick, true) 35 | }, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { browser } from '$app/environment' 2 | import { HoudiniClient, type RequestHandlerArgs } from '$houdini' 3 | 4 | const AUTH_TOKEN_KEY = 'auth_token' 5 | 6 | async function fetchQuery({ 7 | fetch, 8 | text = '', 9 | variables = {}, 10 | }: RequestHandlerArgs) { 11 | const url = 12 | import.meta.env.VITE_GRAPHQL_ENDPOINT || 13 | 'https://readonlydemo.vendure.io/shop-api' 14 | 15 | const headers = {} 16 | headers['Content-Type'] = 'application/json' 17 | 18 | if (browser) { 19 | const token = localStorage.getItem(AUTH_TOKEN_KEY) 20 | if (token) { 21 | headers['Authorization'] = `Bearer ${token}` 22 | } 23 | } 24 | 25 | const result = await fetch(url, { 26 | method: 'POST', 27 | headers, 28 | body: JSON.stringify({ 29 | query: text, 30 | variables, 31 | }), 32 | }) 33 | 34 | if (browser) { 35 | const authToken = result.headers.get('vendure-auth-token') 36 | if (authToken) { 37 | localStorage.setItem(AUTH_TOKEN_KEY, authToken) 38 | } 39 | } 40 | return await result.json() 41 | } 42 | 43 | export default new HoudiniClient(fetchQuery) 44 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | 26 | Vendure & SvelteKit & KitQL & Houdini 27 | 28 | 29 | 30 | 31 | 32 | 33 |
34 | 35 |
36 |