├── src ├── routes │ ├── about │ │ └── +page.svelte │ ├── account │ │ ├── +page.svelte │ │ └── +page.ts │ ├── privacy │ │ └── +page.svelte │ ├── terms │ │ └── +page.svelte │ ├── checkout │ │ ├── +page.ts │ │ ├── success │ │ │ └── [code] │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ └── +page.svelte │ ├── auth │ │ ├── signout │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ │ ├── +page.ts │ │ ├── verify │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ │ └── +page.svelte │ ├── +page.svelte │ ├── braintree │ │ ├── success │ │ │ └── [code] │ │ │ │ ├── +page.svelte │ │ │ │ └── +page.ts │ │ ├── +page.ts │ │ └── +page.svelte │ ├── +layout.ts │ ├── carousel │ │ ├── +page.ts │ │ └── +page.svelte │ ├── collection │ │ └── [slug] │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ ├── product │ │ └── [slug] │ │ │ ├── +page.ts │ │ │ └── +page.svelte │ ├── test │ │ ├── +page.ts │ │ └── +page.svelte │ ├── search │ │ └── +page.svelte │ └── +layout.svelte ├── lib │ ├── components │ │ ├── Newsletter.svelte │ │ ├── Highlights.svelte │ │ ├── AppleButton.svelte │ │ ├── SearchHit.svelte │ │ ├── CheckoutSuccess.svelte │ │ ├── VendureAsset.svelte │ │ ├── Rating.svelte │ │ ├── ThemeSwitcher.svelte │ │ ├── AuthContainer.svelte │ │ ├── ShowHideIcon.svelte │ │ ├── CarouselItem.svelte │ │ ├── InputCheckbox.svelte │ │ ├── Footer.svelte │ │ ├── Gallery.svelte │ │ ├── MetaTags.svelte │ │ ├── Star.svelte │ │ ├── SearchBox.svelte │ │ ├── Account.svelte │ │ ├── NavBar.svelte │ │ ├── ThemeScript.svelte │ │ ├── InputText.svelte │ │ ├── FAQ.svelte │ │ ├── SideBar.svelte │ │ ├── SocialLinks.svelte │ │ ├── CheckoutOrderSummary.svelte │ │ ├── ProductReviews.svelte │ │ ├── Theme.svelte │ │ ├── Cart.svelte │ │ └── GooglePlacesAutocomplete.svelte │ ├── gql │ │ ├── index.ts │ │ └── fragment-masking.ts │ ├── vendure │ │ ├── index.ts │ │ ├── collection.graphql.ts │ │ ├── product.graphql.ts │ │ ├── customer.graphql.ts │ │ └── order.graphql.ts │ ├── utils.ts │ ├── stores.ts │ └── validators.ts ├── index.test.ts ├── app.html ├── app.d.ts ├── environment.d.ts ├── hooks.server.ts └── app.pcss ├── .npmrc ├── static ├── logo.png ├── favicon.png ├── img │ ├── noimg.png │ └── icon-apple.svg ├── robots.txt ├── crossdomain.xml └── logo.svg ├── .gitignore ├── tests └── test.ts ├── playwright.config.ts ├── postcss.config.cjs ├── vite.config.ts ├── tsconfig.json ├── codegen.ts ├── tailwind.config.cjs ├── LICENSE ├── svelte.config.js ├── .env.example ├── package.json └── README.md /src/routes/about/+page.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/account/+page.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/privacy/+page.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/routes/terms/+page.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/lib/components/Newsletter.svelte: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /src/routes/account/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = false -------------------------------------------------------------------------------- /src/routes/checkout/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = false -------------------------------------------------------------------------------- /src/routes/auth/signout/+page.ts: -------------------------------------------------------------------------------- 1 | export const prerender = false -------------------------------------------------------------------------------- /src/lib/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pevey/sveltekit-vendure-starter/HEAD/static/logo.png -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pevey/sveltekit-vendure-starter/HEAD/static/favicon.png -------------------------------------------------------------------------------- /static/img/noimg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pevey/sveltekit-vendure-starter/HEAD/static/img/noimg.png -------------------------------------------------------------------------------- /src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | 3 | describe('sum test', () => { 4 | it('adds 1 + 2 to equal 3', () => { 5 | expect(1 + 2).toBe(3); 6 | }) 7 | }) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /.vercel 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | yarn-error.log -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /account 3 | Disallow: /terms 4 | Disallow: /privacy 5 | Disallow: /auth 6 | Disallow: /api 7 | Disallow: /checkout 8 | Disallow: /search 9 | 10 | User-agent: GPTBot 11 | Disallow: / -------------------------------------------------------------------------------- /static/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('index page has expected h1', async ({ page }) => { 4 | await page.goto('/'); 5 | await expect(page.getByRole('heading', { name: 'Welcome to SvelteKit' })).toBeVisible(); 6 | }); 7 | -------------------------------------------------------------------------------- /src/lib/components/Highlights.svelte: -------------------------------------------------------------------------------- 1 |
2 |

Highlights

3 |
4 | 8 |
9 |
10 | 11 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |
7 |

Your Own Custom Home Page

8 |
9 |
10 | -------------------------------------------------------------------------------- /src/lib/components/AppleButton.svelte: -------------------------------------------------------------------------------- 1 | 2 | 3 | Sign in with Apple 4 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | 3 | const config: PlaywrightTestConfig = { 4 | webServer: { 5 | command: 'npm run build && npm run preview', 6 | port: 4173 7 | }, 8 | testDir: 'tests', 9 | testMatch: /(.+\.)?(test|spec)\.[jt]s/ 10 | }; 11 | 12 | export default config; 13 | -------------------------------------------------------------------------------- /src/routes/braintree/success/[code]/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | {#if data.code} 7 | 8 | {:else} 9 |

Order not found

10 | {/if} -------------------------------------------------------------------------------- /src/routes/checkout/success/[code]/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | {#if data.code} 7 | 8 | {:else} 9 |

Order not found

10 | {/if} -------------------------------------------------------------------------------- /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/routes/+layout.ts: -------------------------------------------------------------------------------- 1 | import { createClient, GetTopLevelCollections } from '$lib/vendure' 2 | 3 | const client = createClient() 4 | 5 | export const prerender = true 6 | 7 | export async function load() { 8 | return { 9 | client, 10 | collections: await client.query(GetTopLevelCollections, {}).toPromise().then((result) => result?.data?.collections?.items) 11 | } 12 | } -------------------------------------------------------------------------------- /src/routes/carousel/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | import { GetCollection } from '$lib/vendure' 3 | 4 | export const load = (async function ({ parent }) { 5 | const { client } = await parent() 6 | return { 7 | result: await client.query(GetCollection, { slug: "electronics" }).toPromise().then((result: any) => result?.data) 8 | } 9 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/routes/collection/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | import { GetCollection } from '$lib/vendure' 3 | 4 | export const load = (async function ({ parent, params }) { 5 | const { client } = await parent() 6 | return { 7 | result: await client.query(GetCollection, { slug: params.slug }).toPromise().then((result: any) => result?.data) 8 | } 9 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/routes/product/[slug]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | import { GetProduct } from '$lib/vendure' 3 | 4 | export const load = (async function ({ parent, params }) { 5 | const { client } = await parent() 6 | return { 7 | client, 8 | product: await client.query(GetProduct, { slug: params.slug }).toPromise().then((result: any) => result?.data?.product) 9 | } 10 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 |
%sveltekit.body%
11 | 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/SearchHit.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/routes/braintree/success/[code]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | 3 | export const prerender = false 4 | 5 | export const load = (async function ({ params, url }) { 6 | const code = params.code 7 | const status = url.searchParams.get('redirect_status') 8 | return { 9 | code, 10 | delay: await new Promise(resolve => setTimeout(resolve, 500)) // allow the order to be processed via the webhook before the page is rendered 11 | } 12 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/routes/checkout/success/[code]/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | 3 | export const prerender = false 4 | 5 | export const load = (async function ({ params, url }) { 6 | const code = params.code 7 | const status = url.searchParams.get('redirect_status') 8 | return { 9 | code, 10 | delay: await new Promise(resolve => setTimeout(resolve, 500)) // allow the order to be processed via the webhook before the page is rendered 11 | } 12 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/routes/braintree/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | import { GenerateBraintreeClientToken } from '$lib/vendure' 3 | 4 | export const prerender = false 5 | 6 | export const load = (async function ({ parent }) { 7 | const { client } = await parent() 8 | const authorization = await client.query(GenerateBraintreeClientToken, {}).toPromise().then((result: any) => result?.data?.generateBraintreeClientToken) 9 | return { 10 | authorization 11 | } 12 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | import type { SalunaConfig } from "../saluna.config" 4 | 5 | declare global { 6 | namespace App { 7 | // interface Error {} 8 | // interface Locals {} 9 | // interface PageData {} 10 | // interface Platform {} 11 | } 12 | declare namespace svelteHTML { 13 | interface HTMLAttributes { 14 | 'on:clickOutside'?: CompositionEventHandler 15 | } 16 | } 17 | interface Window { 18 | __URQL_DATA__: any 19 | } 20 | } 21 | 22 | export {} 23 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite' 2 | import { defineConfig } from 'vitest/config' 3 | import { cjsInterop } from 'vite-plugin-cjs-interop' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | sveltekit(), 8 | cjsInterop({ 9 | dependencies: [ 10 | "@googlemaps/js-api-loader", 11 | ], 12 | }), 13 | ], 14 | // server: { 15 | // host: 'localhost', 16 | // port: 5173 17 | // }, 18 | test: { 19 | include: ['src/**/*.{test,spec}.{js,ts}'] 20 | }, 21 | optimizeDeps: { 22 | exclude: ['@urql/svelte'], 23 | } 24 | }) 25 | -------------------------------------------------------------------------------- /src/lib/components/CheckoutSuccess.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |
7 |

Thank you for your order!

8 |

Your order number is {code}

9 |

← Continue Shopping

10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/lib/components/VendureAsset.svelte: -------------------------------------------------------------------------------- 1 | 8 | {#if preview} 9 | 10 | 11 | 12 | {alt} 13 | 14 | {/if} -------------------------------------------------------------------------------- /src/lib/components/Rating.svelte: -------------------------------------------------------------------------------- 1 | 7 |
8 |

Rating

9 |
10 | {#each stars as star} 11 | {#if rating > star + 0.5} 12 | 13 | {:else if rating > star} 14 | 15 | {:else} 16 | 17 | {/if} 18 | {/each} 19 |
20 |

{rating} out of 5 stars

21 |
22 | -------------------------------------------------------------------------------- /src/lib/components/ThemeSwitcher.svelte: -------------------------------------------------------------------------------- 1 | 7 | {#if (theme === 'dark') || (theme === 'system' && resolvedTheme === 'dark')} 8 | 11 | {:else} 12 | 15 | {/if} -------------------------------------------------------------------------------- /src/lib/vendure/index.ts: -------------------------------------------------------------------------------- 1 | import { Client, cacheExchange, fetchExchange } from '@urql/svelte' 2 | import { dev } from '$app/environment' 3 | import { PUBLIC_SHOPAPI_DEV_URL, PUBLIC_SHOPAPI_PROD_URL } from '$env/static/public' 4 | 5 | export * from './collection.graphql' 6 | export * from './customer.graphql' 7 | export * from './order.graphql' 8 | export * from './product.graphql' 9 | 10 | export const createClient = () => { 11 | return new Client({ 12 | url: dev ? PUBLIC_SHOPAPI_DEV_URL : PUBLIC_SHOPAPI_PROD_URL, 13 | exchanges: [cacheExchange, fetchExchange], 14 | fetchOptions: { 15 | credentials: 'include' 16 | } 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /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": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 15 | // 16 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 17 | // from the referenced tsconfig.json - TypeScript does not merge them in 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/auth/+page.ts: -------------------------------------------------------------------------------- 1 | import { superValidate } from 'sveltekit-superforms' 2 | import { zod } from 'sveltekit-superforms/adapters' 3 | import { signInReq, signUpReq, forgotReq, resetReq } from '$lib/validators' 4 | 5 | export async function load() { 6 | const signInForm = await superValidate(zod(signInReq), { id: 'signIn' }) 7 | const signUpForm = await superValidate(zod(signUpReq), { id: 'signUp' }) 8 | const forgotForm = await superValidate(zod(forgotReq), { id: 'forgot' }) 9 | const resetForm = await superValidate(zod(resetReq), { id: 'reset' }) 10 | return { 11 | signUpForm, 12 | signInForm, 13 | forgotForm, 14 | resetForm 15 | } 16 | } -------------------------------------------------------------------------------- /src/routes/auth/verify/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | import { browser } from '$app/environment' 3 | import { VerifyCustomerAccount } from '$lib/vendure' 4 | 5 | export const prerender = false 6 | 7 | export const load = (async ({ url, parent }) => { 8 | // vendure token renamed to code so as to not conflict with cf token if used 9 | const code = url.searchParams.get('token') || '' 10 | let result: any 11 | if (browser) { 12 | const { client } = await parent() 13 | result = await client.mutation(VerifyCustomerAccount, { token: code }).toPromise() 14 | } 15 | return { 16 | result: result?.data?.verifyCustomerAccount?.__typename, 17 | code 18 | } 19 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/routes/test/+page.ts: -------------------------------------------------------------------------------- 1 | import type { PageLoad } from './$types' 2 | import { superValidate } from 'sveltekit-superforms' 3 | import { zod } from 'sveltekit-superforms/adapters' 4 | import { braintreeCheckoutReq } from '$lib/validators' 5 | import { GenerateBraintreeClientToken } from '$lib/vendure' 6 | 7 | export const prerender = false 8 | 9 | export const load = (async function ({ parent }) { 10 | const { client } = await parent() 11 | const authorization = await client.query(GenerateBraintreeClientToken, { includeCustomerId: false }).toPromise().then((result: any) => result?.data?.generateBraintreeClientToken) 12 | const form = await superValidate(zod(braintreeCheckoutReq)) 13 | return { 14 | authorization, 15 | form 16 | } 17 | }) satisfies PageLoad -------------------------------------------------------------------------------- /src/lib/components/AuthContainer.svelte: -------------------------------------------------------------------------------- 1 |
16 |
17 |
18 |
19 | 20 |
21 |
22 |
23 |
24 | -------------------------------------------------------------------------------- /src/lib/components/ShowHideIcon.svelte: -------------------------------------------------------------------------------- 1 | 15 |
16 | 17 | 20 |
21 | -------------------------------------------------------------------------------- /codegen.ts: -------------------------------------------------------------------------------- 1 | import type { CodegenConfig } from '@graphql-codegen/cli' 2 | import 'dotenv/config' 3 | 4 | const IS_DEV = process.env.APP_ENV === 'dev' 5 | 6 | const config: CodegenConfig = { 7 | schema: IS_DEV? process.env.PUBLIC_SHOPAPI_DEV_URL: process.env.PUBLIC_SHOPAPI_PROD_URL, 8 | // watch: true, 9 | // documents: ['src/**/*.{ts,svelte,graphql.ts}', '!src/lib/gql/*'], 10 | documents: ['src/**/*.{ts,svelte,graphql.ts}', '!src/lib/gql/*'], 11 | ignoreNoDocuments: true, 12 | generates: { 13 | 'src/lib/gql/': { 14 | preset: 'client', 15 | presetConfig: { 16 | gqlTagName: 'gql', 17 | // fragmentMasking: false, 18 | }, 19 | plugins: ['typescript'], 20 | config: { 21 | useTypeImports: true, // This is needed to avoid Vite/SvelteKit import errors 22 | scalars: { 23 | // This tells codegen that the `Money` scalar is a number 24 | Money: 'number', 25 | } 26 | } 27 | }, 28 | } 29 | } 30 | export default config -------------------------------------------------------------------------------- /src/environment.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | // Here we declare the members of the process.env object, so that we 4 | // can use them in our application code in a type-safe manner. 5 | declare global { 6 | namespace NodeJS { 7 | interface ProcessEnv { 8 | NODE_ENV: string 9 | ORIGIN: string 10 | VENDURE_SHOPAPI_DEV_URL: string 11 | VENDURE_SHOPAPI_PROD_URL: string 12 | PUBLIC_REQUIRE_EMAIL_VERIFICATION: boolean 13 | PUBLIC_DEFAULT_CURRENCY: string 14 | PUBLIC_LOCAL_PICKUP_CODE: string 15 | PUBLIC_SITE_NAME: string 16 | PUBLIC_SITE_DESCRIPTION: string 17 | PUBLIC_SITE_LOGO: string 18 | PUBLIC_SITE_IMAGE: string 19 | PUBLIC_SITE_URL: string 20 | PUBLIC_TWITTER_HANDLE: string 21 | PUBLIC_TWITTER_SITE: string 22 | PUBLIC_TWITTER_CARD_TYPE: string 23 | PUBLIC_STRIPE_KEY: string 24 | PUBLIC_TURNSTILE_SITE_KEY: string 25 | SECRET_TURNSTILE_KEY: string 26 | CLOUDFLARE_ACCESS_ID: string 27 | CLOUDFLARE_ACCESS_SECRET: string 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | import plugin from 'tailwindcss/plugin' 2 | import typography from '@tailwindcss/typography' 3 | import forms from '@tailwindcss/forms' 4 | 5 | /** @type {import('tailwindcss').Config}*/ 6 | const config = { 7 | content: ["./src/**/*.{html,js,svelte,ts}"], 8 | darkMode: 'selector', 9 | theme: { 10 | extend: { 11 | colors: { 12 | 'orange': { 13 | 50: '#FFF2EE', 14 | 100: '#FFDBD1', 15 | 200: '#FFC7B7', 16 | 300: '#FFAF9B', 17 | 400: '#FF977D', 18 | 500: '#FF8E72', 19 | 600: '#FF7A59', 20 | 700: '#FF6C47', 21 | 800: '#FF5621', 22 | 900: '#FF460C' 23 | } 24 | } 25 | } 26 | }, 27 | plugins: [ 28 | typography, 29 | forms, 30 | plugin(function ({ addVariant, matchUtilities, theme }) { 31 | addVariant('hocus', ['&:hover', '&:focus']); 32 | // Square utility 33 | matchUtilities({ 34 | square: (value) => ({ 35 | width: value, 36 | height: value, 37 | }), 38 | }, 39 | { values: theme('spacing') } 40 | ) 41 | }) 42 | ] 43 | } 44 | 45 | module.exports = config; 46 | -------------------------------------------------------------------------------- /src/lib/vendure/collection.graphql.ts: -------------------------------------------------------------------------------- 1 | import { gql } from '$lib/gql' 2 | 3 | export const Collection = gql(` 4 | fragment Collection on Collection { 5 | id 6 | name 7 | slug 8 | description 9 | featuredAsset { 10 | id 11 | preview 12 | } 13 | } 14 | `) 15 | 16 | export const GetCollection = gql(` 17 | query GetCollection($slug: String!, $skip: Int, $take: Int) { 18 | collection(slug: $slug) { 19 | ...Collection 20 | } 21 | search( 22 | input: { 23 | collectionSlug: $slug, 24 | groupByProduct: true, 25 | skip: $skip, 26 | take: $take 27 | } 28 | ) { 29 | items { 30 | ...SearchResult 31 | } 32 | totalItems 33 | } 34 | } 35 | `) 36 | 37 | export const GetCollections = gql(` 38 | query GetCollections($options: CollectionListOptions) { 39 | collections { 40 | items { 41 | ...Collection 42 | } 43 | totalItems 44 | } 45 | } 46 | `) 47 | 48 | export const GetTopLevelCollections = gql(` 49 | query GetTopLevelCollections { 50 | collections(options: { topLevelOnly: true }) { 51 | items { 52 | ...Collection 53 | } 54 | totalItems 55 | } 56 | } 57 | `) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Lacey Pevey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /static/img/icon-apple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | export const clickOutside = (node: HTMLElement) => { 2 | const handleClick = (event: MouseEvent) => { 3 | if (node && !node.contains(event.target as Node) && !event.defaultPrevented) { 4 | node.dispatchEvent( 5 | new CustomEvent('clickOutside') 6 | ) 7 | } 8 | } 9 | document.addEventListener('click', handleClick, true) 10 | return { 11 | destroy() { 12 | document.removeEventListener('click', handleClick, true) 13 | } 14 | } 15 | } 16 | 17 | export const formatCurrency = function(value: number, currencyCode: string, locale?: string) { 18 | // See Vendure docs for more info: 19 | // https://docs.vendure.io/guides/core-concepts/money/#displaying-monetary-values 20 | const majorUnits = value / 100 21 | try { 22 | // If no `locale` is provided, the browser's default locale will be used. 23 | return new Intl.NumberFormat(locale, { 24 | style: 'currency', 25 | currency: currencyCode, 26 | currencyDisplay: 'symbol' 27 | }).format(majorUnits) 28 | } catch (e: any) { 29 | // A fallback in case the NumberFormat fails for any reason 30 | return majorUnits.toFixed(2) 31 | } 32 | } -------------------------------------------------------------------------------- /src/lib/components/CarouselItem.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 |
23 |

24 | {title} 25 |

26 |
27 |
-------------------------------------------------------------------------------- /src/routes/auth/signout/+page.svelte: -------------------------------------------------------------------------------- 1 | 27 | {#if success === true} 28 |
29 |
30 |

You have been successfully signed out.

31 |

← Return to the home page.

32 |
33 |
34 | {:else} 35 |
36 |
37 |

There was an error signing you out. Please try again.

38 |
39 |
40 | {/if} 41 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from '@sveltejs/adapter-node' 2 | // import adapter from '@sveltejs/adapter-vercel' 3 | // import adapter from '@sveltejs/adapter-cloudflare' 4 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte' 5 | import { join } from 'path' 6 | import 'dotenv/config' 7 | 8 | /** @type {import('@sveltejs/kit').Config} */ 9 | const config = { 10 | preprocess: vitePreprocess({ 11 | style: { 12 | css: { 13 | postcss: join(process.cwd(), 'postcss.config.cjs') 14 | } 15 | } 16 | }), 17 | kit: { 18 | // uncomment for Cloudflare 19 | // adapter: adapter({ 20 | // routes: { 21 | // include: ['/*'] 22 | // } 23 | // }), 24 | // uncomment for Node or Vercel 25 | adapter: adapter(), 26 | // prerender: { 27 | // handleHttpError: 'ignore', 28 | // }, 29 | alias: { 30 | '$src/*': 'src/*' 31 | } 32 | // csp: { 33 | // directives: { 34 | // 'script-src': ['self', 'https://laroastingco.com/', 'https://challenges.cloudflare.com/', 'https://js.stripe.com/'], 35 | // 'img-src': ['self', 'https://laroastingco.com/', 'data:', process.env.ORIGIN, 'https://challenges.cloudflare.com/', 'https://js.stripe.com/'], 36 | // } 37 | // } 38 | }, 39 | } 40 | 41 | export default config 42 | -------------------------------------------------------------------------------- /src/lib/components/InputCheckbox.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 15 | 16 | 28 | {#if label} 29 | 30 | {/if} 31 | 32 | {#if $errors?.length} 33 | {#each $errors as err} 34 | {err} 35 | {/each} 36 | {/if} 37 | -------------------------------------------------------------------------------- /src/lib/components/Footer.svelte: -------------------------------------------------------------------------------- 1 | 5 |
6 |
7 | 27 |
28 | 29 |
30 |

© 2006 - {year} Louisiana Roasting Company, LLC. All rights reserved.

31 |
32 |
-------------------------------------------------------------------------------- /src/hooks.server.ts: -------------------------------------------------------------------------------- 1 | import type { Handle } from '@sveltejs/kit' 2 | 3 | export const handle: Handle = async ({ event, resolve }) => { 4 | 5 | // Required for all paths 6 | const response = await resolve(event) 7 | 8 | // SECURITY HEADERS 9 | // CSP directives are set elsewhere in svelte.config.js and added automatically by SvelteKit. 10 | // CSRF mitigation in SvelteKit is handled by header-checking and is enabled by default. More secure token-based CSRF mitigation must be added manually. 11 | // Token-based CSRF mitigation for the most sensitive endpoints/form actions is handled by Cloudflare Turnstile. 12 | // The headers below provide additional security against XSS, clickjacking, MIME-type sniffing, and other attacks. 13 | response.headers.set('X-Frame-Options', 'DENY') 14 | response.headers.set('X-Content-Type-Options', 'nosniff') 15 | response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') 16 | response.headers.set('Permissions-Policy', 'payment=(self "https://js.stripe.com/"), accelerometer=(), camera=(), display-capture=(), encrypted-media=(), fullscreen=(), gyroscope=(), hid=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), picture-in-picture=(), publickey-credentials-get=(), sync-xhr=(), usb=(), xr-spatial-tracking=(), geolocation=()') 17 | 18 | return response 19 | } -------------------------------------------------------------------------------- /src/lib/stores.ts: -------------------------------------------------------------------------------- 1 | import { type Writable, writable } from 'svelte/store' 2 | import type { FragmentType } from '$lib/gql' 3 | import { ActiveOrder, Customer } from '$lib/vendure' 4 | 5 | export const cookiesDisabledStore: Writable = writable(false) 6 | 7 | export const cartStore: Writable|null> = writable() 8 | 9 | export const userStore: Writable|null> = writable() 10 | 11 | export interface ThemeStore { 12 | /** List of all available theme names */ 13 | themes: string[]; 14 | /** Forced theme name for the current page */ 15 | forcedTheme?: string; 16 | /** Update the theme */ 17 | /** Active theme name */ 18 | theme?: string; 19 | /** If `enableSystem` is true and the active theme is "system", this returns whether the system preference resolved to "dark" or "light". Otherwise, identical to `theme` */ 20 | resolvedTheme?: string; 21 | /** If enableSystem is true, returns the System theme preference ("dark" or "light"), regardless what the active theme is */ 22 | systemTheme?: 'dark' | 'light'; 23 | } 24 | 25 | export const themeStore = writable({ 26 | themes: [], 27 | forcedTheme: undefined, 28 | theme: undefined, 29 | resolvedTheme: undefined, 30 | systemTheme: undefined 31 | }) 32 | export const setTheme = (theme: string): void => { 33 | themeStore.update((store) => ({ ...store, theme })) 34 | } -------------------------------------------------------------------------------- /src/routes/search/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 | 23 | 24 | {#each hits as hit} 25 | 26 | {:else} 27 | {#if $q} 28 |

No results found.

29 | {:else} 30 |

Enter a search term.

31 | {/if} 32 | {/each} 33 |
-------------------------------------------------------------------------------- /src/lib/components/Gallery.svelte: -------------------------------------------------------------------------------- 1 | 12 |
13 | 14 |
15 | 16 |
17 | 18 | 31 |
-------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | 3 | # Setting this will help ensure that you don't get errors from SvelteKit's built-in CSRF protection 4 | ORIGIN=http://localhost:5173 5 | 6 | PUBLIC_SHOPAPI_DEV_URL=http://localhost:3000/shop-api 7 | PUBLIC_SHOPAPI_PROD_URL=http://localhost:3000/shop-api 8 | PUBLIC_REQUIRE_EMAIL_VERIFICATION=true 9 | PUBLIC_DEFAULT_CURRENCY=USD 10 | # To enable local pickup as a delivery option, enter the code of the Vendure shipping method that will be used for local pickup 11 | # Leave empty to disable local pickup 12 | #PUBLIC_LOCAL_PICKUP="local-pickup" 13 | PUBLIC_LOCAL_PICKUP_CODE="" 14 | 15 | # These settings will be use to generate your SEO tags 16 | PUBLIC_SITE_NAME="Store Name" 17 | PUBLIC_SITE_DESCRIPTION="Store description" 18 | PUBLIC_SITE_LOGO="http://localhost:5173/logo.png" 19 | PUBLIC_SITE_IMAGE="http://localhost:5173/logo.png" 20 | PUBLIC_SITE_URL="http://localhost:5173" 21 | 22 | # Optional settings for Twitter SEO tags 23 | PUBLIC_TWITTER_HANDLE="" 24 | PUBLIC_TWITTER_SITE="" 25 | PUBLIC_TWITTER_CARD_TYPE="summary_large_image" 26 | 27 | # Required settings for Stripe checkout 28 | PUBLIC_STRIPE_KEY="pk_test_123..." 29 | PUBLIC_STRIPE_REDIRECT_URL=http://localhost:5173/checkout/success 30 | 31 | # Required settings for Braintree checkout 32 | PUBLIC_GOOGLE_PLACES_API_KEY=A... 33 | 34 | # Optional settings for enabling Cloudflare Turnstile 35 | # Leave these empty to disable Turnstile 36 | # PUBLIC_TURNSTILE_SITE_KEY="0x4AAAAAAAL2X2PR254azwGy" # test key that will work only on localhost domain 37 | # SECRET_TURNSTILE_KEY="0x4AAAAAAAL2XwkH3pt8-PJiDFsMQ3gKots" # test key that will work only on localhost domain 38 | PUBLIC_TURNSTILE_SITE_KEY="" 39 | SECRET_TURNSTILE_KEY="" -------------------------------------------------------------------------------- /src/routes/auth/verify/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | {#if data.result === 'CurrentUser'} 30 | 31 |
32 |
33 |

Your email has been verified.

34 |

← Return to the home page.

35 |
36 |
37 | {:else} 38 |
39 |
40 |

There was an error verifying your email. Please try again.

41 |

← Return to the home page.

42 |
43 |
44 | {/if} -------------------------------------------------------------------------------- /src/lib/components/MetaTags.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | -------------------------------------------------------------------------------- /src/lib/components/Star.svelte: -------------------------------------------------------------------------------- 1 | 4 | 8 | {#if filled} 9 | {#if filled === 'half'} 10 | 13 | {:else} 14 | 17 | {/if} 18 | {:else} 19 | 22 | {/if} -------------------------------------------------------------------------------- /src/routes/carousel/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 15 | 37 | 59 | 60 | 61 |
62 |
63 |

Your Own Custom Home Page

64 |
65 |
66 | -------------------------------------------------------------------------------- /src/lib/components/SearchBox.svelte: -------------------------------------------------------------------------------- 1 | 20 | 36 |
37 | 42 |
-------------------------------------------------------------------------------- /src/lib/components/Account.svelte: -------------------------------------------------------------------------------- 1 | 16 | {#if me} 17 | 21 | {:else} 22 | 23 | 27 | 28 | {/if} 29 | 39 | 60 | -------------------------------------------------------------------------------- /src/lib/components/NavBar.svelte: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sveltekit-vendure-starter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently 'vite dev' 'pnpm codegen --watch'", 7 | "build": "shx rm -rf ./build && pnpm codegen && vite build", 8 | "preview": "vite preview", 9 | "test": "pnpm test:integration && pnpm test:unit", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 12 | "test:integration": "playwright test", 13 | "test:unit": "vitest", 14 | "codegen": "graphql-codegen" 15 | }, 16 | "devDependencies": { 17 | "@googlemaps/js-api-loader": "^1.16.6", 18 | "@graphql-codegen/cli": "^5.0.2", 19 | "@graphql-codegen/client-preset": "^4.2.5", 20 | "@melt-ui/svelte": "^0.76.3", 21 | "@parcel/watcher": "^2.4.1", 22 | "@playwright/test": "^1.43.0", 23 | "@sveltejs/adapter-cloudflare": "^4.2.1", 24 | "@sveltejs/adapter-node": "^5.0.1", 25 | "@sveltejs/adapter-vercel": "^5.2.0", 26 | "@sveltejs/kit": "^2.5.5", 27 | "@sveltejs/vite-plugin-svelte": "^3.0.2", 28 | "@tailwindcss/forms": "^0.5.6", 29 | "@tailwindcss/typography": "^0.5.12", 30 | "@types/braintree-web-drop-in": "^1.39.3", 31 | "@types/google.maps": "^3.55.7", 32 | "autoprefixer": "^10.4.19", 33 | "braintree-web-drop-in": "^1.42.0", 34 | "concurrently": "^8.2.2", 35 | "dotenv": "^16.4.5", 36 | "graphql": "^16.8.1", 37 | "lucide-svelte": "^0.366.0", 38 | "shx": "^0.3.4", 39 | "svelte": "^4.2.12", 40 | "svelte-check": "^3.6.9", 41 | "svelte-meta-tags": "^3.1.1", 42 | "sveltekit-search-params": "^2.1.2", 43 | "sveltekit-stripe": "^3.1.3", 44 | "sveltekit-superfetch": "^3.0.3", 45 | "sveltekit-superforms": "^2.12.2", 46 | "sveltekit-turnstile": "^1.2.0", 47 | "svoast": "^2.4.4", 48 | "tailwindcss": "^3.4.2", 49 | "tslib": "^2.4.1", 50 | "typescript": "^5.4.4", 51 | "vite": "5.2.8", 52 | "vite-plugin-cjs-interop": "^2.1.0", 53 | "vitest": "1.4.0", 54 | "xss": "^1.0.15", 55 | "zod": "^3.22.4" 56 | }, 57 | "dependencies": { 58 | "@urql/svelte": "^4.1.1" 59 | }, 60 | "type": "module" 61 | } 62 | -------------------------------------------------------------------------------- /src/app.pcss: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in PostCSS syntax */ 2 | @tailwind base; 3 | @tailwind components; 4 | @tailwind utilities; 5 | 6 | :root { 7 | --primary-color: rgba(168, 85, 247, 1); 8 | --svoast-success-colour: #65a30d; 9 | } 10 | 11 | /* Override default focus colors for tailwindcss-forms https://github.com/tailwindlabs/tailwindcss-forms */ 12 | [type='text']:focus, 13 | [type='email']:focus, 14 | [type='url']:focus, 15 | [type='password']:focus, 16 | [type='number']:focus, 17 | [type='date']:focus, 18 | [type='datetime-local']:focus, 19 | [type='month']:focus, 20 | [type='search']:focus, 21 | [type='tel']:focus, 22 | [type='checkbox']:focus, 23 | [type='radio']:focus, 24 | [type='time']:focus, 25 | [type='week']:focus, 26 | [multiple]:focus, 27 | textarea:focus, 28 | select:focus { 29 | --tw-ring-color: var(--primary-color); 30 | border-color: var(--primary-color); 31 | } 32 | 33 | .grow-on-hover { 34 | @apply transition ease-in-out hover:-translate-y-1 hover:scale-110; 35 | } 36 | 37 | /* FORMS */ 38 | [data-label="gpac"] { 39 | @apply m-1 text-sm; 40 | } 41 | [data-text-input="gpac"] { 42 | @apply w-full p-3 rounded border-gray-400 ring-0 focus:ring-0 focus:border-gray-800; 43 | } 44 | [data-checkbox-input="gpac"] { 45 | @apply my-2 p-2 rounded border-gray-400 ring-0 focus:ring-0 focus:border-gray-800; 46 | } 47 | [data-fs-label], .label { 48 | @apply block m-1 mt-4 text-base text-gray-600 font-semibold; 49 | } 50 | [data-fs-control], [data-text-input="auth"], .input { 51 | @apply w-full block py-3 px-4 text-gray-900 placeholder-gray-400 border border-gray-200 focus:border-violet-600 rounded-md; 52 | } 53 | [data-fs-field-errors], .validation, .invalid { 54 | @apply block my-2 text-sm text-red-600; 55 | } 56 | .button { 57 | @apply inline-block w-full my-4 py-3 px-5 text-sm font-semibold text-white bg-orange-900 hover:bg-gray-900 rounded-md transition duration-200 58 | } 59 | .checkbox { 60 | @apply mr-2 h-5 w-5 rounded-md text-violet-500 border border-gray-200 focus:border focus:border-gray-200 focus:ring-0 checked:border-2 checked:border-violet-500 checked:ring-0; 61 | } 62 | .message { 63 | @apply block my-8 text-lg font-semibold text-gray-600; 64 | } -------------------------------------------------------------------------------- /src/lib/components/ThemeScript.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | {@html themeScript} 51 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 47 | 48 | 49 | {#if naked} 50 | 51 | {:else} 52 | 53 | 54 |