+ user: User
+}) => {
+ const { data: dbCustomer, error } = await supabaseServiceRole
+ .from("stripe_customers")
+ .select("stripe_customer_id")
+ .eq("user_id", user.id)
+ .single()
+
+ if (error && error.code != "PGRST116") {
+ // PGRST116 == no rows
+ return { error: error }
+ }
+
+ if (dbCustomer?.stripe_customer_id) {
+ return { customerId: dbCustomer.stripe_customer_id }
+ }
+
+ // Fetch data needed to create customer
+ const { data: profile, error: profileError } = await supabaseServiceRole
+ .from("profiles")
+ .select(`full_name, website, company_name`)
+ .eq("id", user.id)
+ .single()
+ if (profileError) {
+ return { error: profileError }
+ }
+
+ // Create a stripe customer
+ let customer
+ try {
+ customer = await stripe.customers.create({
+ email: user.email,
+ name: profile.full_name ?? "",
+ metadata: {
+ user_id: user.id,
+ company_name: profile.company_name ?? "",
+ website: profile.website ?? "",
+ },
+ })
+ } catch (e) {
+ return { error: e }
+ }
+
+ if (!customer.id) {
+ return { error: "Unknown stripe user creation error" }
+ }
+
+ // insert instead of upsert so we never over-write. PK ensures later attempts error.
+ const { error: insertError } = await supabaseServiceRole
+ .from("stripe_customers")
+ .insert({
+ user_id: user.id,
+ stripe_customer_id: customer.id,
+ updated_at: new Date(),
+ })
+
+ if (insertError) {
+ return { error: insertError }
+ }
+
+ return { customerId: customer.id }
+}
+
+export const fetchSubscription = async ({
+ customerId,
+}: {
+ customerId: string
+}) => {
+ // Fetch user's subscriptions
+ let stripeSubscriptions
+ try {
+ stripeSubscriptions = await stripe.subscriptions.list({
+ customer: customerId,
+ limit: 100,
+ status: "all",
+ })
+ } catch (e) {
+ return { error: e }
+ }
+
+ // find "primary". The user may have several old ones, we want an active one (including trials, and past_due in grace period).
+ const primaryStripeSubscription = stripeSubscriptions.data.find((x) => {
+ return (
+ x.status === "active" ||
+ x.status === "trialing" ||
+ x.status === "past_due"
+ )
+ })
+ let appSubscription = null
+ if (primaryStripeSubscription) {
+ const productId =
+ primaryStripeSubscription?.items?.data?.[0]?.price.product ?? ""
+ appSubscription = pricingPlans.find((x) => {
+ return x.stripe_product_id === productId
+ })
+ if (!appSubscription) {
+ return {
+ error:
+ "Stripe subscription does not have matching app subscription in pricing_plans.ts (via product id match)",
+ }
+ }
+ }
+ let primarySubscription = null
+ if (primaryStripeSubscription && appSubscription) {
+ primarySubscription = {
+ stripeSubscription: primaryStripeSubscription,
+ appSubscription: appSubscription,
+ }
+ }
+
+ const hasEverHadSubscription = stripeSubscriptions.data.length > 0
+
+ return {
+ primarySubscription,
+ hasEverHadSubscription,
+ }
+}
diff --git a/src/routes/(marketing)/+layout.svelte b/src/routes/(marketing)/+layout.svelte
new file mode 100644
index 0000000..a522c77
--- /dev/null
+++ b/src/routes/(marketing)/+layout.svelte
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/(marketing)/+page.svelte b/src/routes/(marketing)/+page.svelte
new file mode 100644
index 0000000..b287195
--- /dev/null
+++ b/src/routes/(marketing)/+page.svelte
@@ -0,0 +1,400 @@
+
+
+
+ {WebsiteName}
+
+
+ {@html jsonldScript}
+
+
+
+
+
+
+ SaaS Starter Demo
+
+
+
+ The
+ open source,
+ fast, and
+ free to host
+ SaaS template
+
+
+
+
+
+
+
+
+
+
+ Explore the Features
+
+
+ And try them on this
+
+ fully functional demo
+
+
+
+
+
+ {#each features as feature}
+
+ {/each}
+
+
+
+
+
+
+
+ See it in Action
+
+
+
+
+
+
+
+
+ Our
webpage is the best example of SaaS Starter with style and real content.
+
+
+
+
+
+
+
+
diff --git a/src/routes/(marketing)/+page.ts b/src/routes/(marketing)/+page.ts
new file mode 100644
index 0000000..176ae64
--- /dev/null
+++ b/src/routes/(marketing)/+page.ts
@@ -0,0 +1 @@
+export const prerender = true
diff --git a/src/routes/(marketing)/auth/callback/+server.js b/src/routes/(marketing)/auth/callback/+server.js
new file mode 100644
index 0000000..ef30641
--- /dev/null
+++ b/src/routes/(marketing)/auth/callback/+server.js
@@ -0,0 +1,27 @@
+// src/routes/auth/callback/+server.js
+import { redirect } from "@sveltejs/kit"
+import { isAuthApiError } from "@supabase/supabase-js"
+
+export const GET = async ({ url, locals: { supabase } }) => {
+ const code = url.searchParams.get("code")
+ if (code) {
+ try {
+ await supabase.auth.exchangeCodeForSession(code)
+ } catch (error) {
+ // If you open in another browser, need to redirect to login.
+ // Should not display error
+ if (isAuthApiError(error)) {
+ throw redirect(303, "/login/sign_in?verified=true")
+ } else {
+ throw error
+ }
+ }
+ }
+
+ const next = url.searchParams.get("next")
+ if (next) {
+ throw redirect(303, next)
+ }
+
+ throw redirect(303, "/account")
+}
diff --git a/src/routes/(marketing)/blog/(posts)/+layout.svelte b/src/routes/(marketing)/blog/(posts)/+layout.svelte
new file mode 100644
index 0000000..1280fd0
--- /dev/null
+++ b/src/routes/(marketing)/blog/(posts)/+layout.svelte
@@ -0,0 +1,76 @@
+
+
+
+ {pageTitle}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {@html jsonldScript}
+
+
+
+ {#if currentPost == null}
+ Blog post not found
+ {:else}
+
+ {currentPost.parsedDate?.toLocaleDateString("en-US", {
+ month: "short",
+ day: "numeric",
+ year: "numeric",
+ })}
+
+ {currentPost.title}
+
+ {/if}
+
diff --git a/src/routes/(marketing)/blog/(posts)/awesome_post/+page.svelte b/src/routes/(marketing)/blog/(posts)/awesome_post/+page.svelte
new file mode 100644
index 0000000..36e36e2
--- /dev/null
+++ b/src/routes/(marketing)/blog/(posts)/awesome_post/+page.svelte
@@ -0,0 +1,26 @@
+A sample post
+
+
+ This is a sample blog post to demonstrate the blog engine. It shows some of
+ the formatting options included in the template.
+
+
+Section Titles are great
+
+As are more paragraphs.
+
+
+ Block quotes are styled
+
+
+# Code blocks work too!
+npm install
+
+
+
+ Check out more formatting options like lists, headers, and more in the tailwind/typograpy docs.
+
diff --git a/src/routes/(marketing)/blog/(posts)/example_blog_post/+page.svelte b/src/routes/(marketing)/blog/(posts)/example_blog_post/+page.svelte
new file mode 100644
index 0000000..36e36e2
--- /dev/null
+++ b/src/routes/(marketing)/blog/(posts)/example_blog_post/+page.svelte
@@ -0,0 +1,26 @@
+A sample post
+
+
+ This is a sample blog post to demonstrate the blog engine. It shows some of
+ the formatting options included in the template.
+
+
+Section Titles are great
+
+As are more paragraphs.
+
+
+ Block quotes are styled
+
+
+# Code blocks work too!
+npm install
+
+
+
+ Check out more formatting options like lists, headers, and more in the tailwind/typograpy docs.
+
diff --git a/src/routes/(marketing)/blog/(posts)/how_we_built_our_41kb_saas_website/+page.svelte b/src/routes/(marketing)/blog/(posts)/how_we_built_our_41kb_saas_website/+page.svelte
new file mode 100644
index 0000000..f2a8758
--- /dev/null
+++ b/src/routes/(marketing)/blog/(posts)/how_we_built_our_41kb_saas_website/+page.svelte
@@ -0,0 +1,35 @@
+How to use this template you to bootstrap your own site.
+
+
+ We've written a detailed blog post about how we took this template, and
+ created a real SaaS company website. Topics include:
+
+
+
+ - Optimizing the stack for performance and developer productivity
+ - Creating rich interactive animations with Svelte
+ - Creating pixel perfect designs without rasterization
+ - Speed measurements: how we kept it small and lightning fast
+
+
+Read the Blog Post
+
+
+ The blog post is over on criticalmoments.io, a page which uses this boilerplate as a starting point.
+
+
+
+ If you are looking for examples of blog posts with rich content rendered
+ inside this template, checkout the other demo posts here.
+
diff --git a/src/routes/(marketing)/blog/+layout.ts b/src/routes/(marketing)/blog/+layout.ts
new file mode 100644
index 0000000..176ae64
--- /dev/null
+++ b/src/routes/(marketing)/blog/+layout.ts
@@ -0,0 +1 @@
+export const prerender = true
diff --git a/src/routes/(marketing)/blog/+page.svelte b/src/routes/(marketing)/blog/+page.svelte
new file mode 100644
index 0000000..0d973c4
--- /dev/null
+++ b/src/routes/(marketing)/blog/+page.svelte
@@ -0,0 +1,47 @@
+
+
+
+ {blogInfo.name}
+
+
+
+
diff --git a/src/routes/(marketing)/blog/posts.ts b/src/routes/(marketing)/blog/posts.ts
new file mode 100644
index 0000000..6199262
--- /dev/null
+++ b/src/routes/(marketing)/blog/posts.ts
@@ -0,0 +1,52 @@
+export const blogInfo = {
+ name: "SaaS Starter Blog",
+ description: "A sample blog",
+}
+
+export type BlogPost = {
+ link: string
+ date: string // date is a string 'YYYY-MM-DD'
+ title: string
+ description: string
+ parsedDate?: Date // Optional because it's added dynamically
+}
+
+// Update this list with the actual blog post list
+// Create a page in the "(posts)" directory for each entry
+const blogPosts: BlogPost[] = [
+ {
+ title: "How we built a beautiful 41kb SaaS website with this template",
+ description: "How to use this template you to bootstrap your own site.",
+ link: "/blog/how_we_built_our_41kb_saas_website",
+ date: "2024-03-10",
+ },
+ {
+ title: "Example Blog Post 2",
+ description: "Even more example content!",
+ link: "/blog/awesome_post",
+ date: "2022-9-23",
+ },
+ {
+ title: "Example Blog Post",
+ description: "A sample blog post, showing our blog engine",
+ link: "/blog/example_blog_post",
+ date: "2023-03-13",
+ },
+]
+
+// Parse post dates from strings to Date objects
+for (const post of blogPosts) {
+ if (!post.parsedDate) {
+ const dateParts = post.date.split("-")
+ post.parsedDate = new Date(
+ parseInt(dateParts[0]),
+ parseInt(dateParts[1]) - 1,
+ parseInt(dateParts[2]),
+ ) // Note: months are 0-based
+ }
+}
+
+export const sortedBlogPosts = blogPosts.sort(
+ (a: BlogPost, b: BlogPost) =>
+ (b.parsedDate?.getTime() ?? 0) - (a.parsedDate?.getTime() ?? 0),
+)
diff --git a/src/routes/(marketing)/blog/rss.xml/+server.ts b/src/routes/(marketing)/blog/rss.xml/+server.ts
new file mode 100644
index 0000000..6a11fd1
--- /dev/null
+++ b/src/routes/(marketing)/blog/rss.xml/+server.ts
@@ -0,0 +1,37 @@
+import { sortedBlogPosts, blogInfo } from "../posts"
+
+const encodeXML = (str: string) =>
+ str
+ .replace(/&/g, "&")
+ .replace(//g, ">")
+ .replace(/"/g, """)
+ .replace(/'/g, "'")
+
+/** @type {import('./$types').RequestHandler} */
+export function GET({ url }) {
+ const headers = {
+ "Cache-Control": "max-age=0, s-maxage=3600",
+ "Content-Type": "application/xml",
+ }
+
+ let body = `
+
+ ${blogInfo.name}
+ ${url.origin}/blog
+ ${blogInfo.description}
+ `
+ for (const post of sortedBlogPosts) {
+ body += `
+ -
+ ${encodeXML(post.title)}
+ ${encodeXML(post.description)}
+ ${url.origin + post.link}/
+ ${post.parsedDate?.toUTCString()}
+
\n`
+ }
+ body += ` \n\n`
+ return new Response(body, {
+ headers: headers,
+ })
+}
diff --git a/src/routes/(marketing)/contact_us/+page.server.ts b/src/routes/(marketing)/contact_us/+page.server.ts
new file mode 100644
index 0000000..ffaceb4
--- /dev/null
+++ b/src/routes/(marketing)/contact_us/+page.server.ts
@@ -0,0 +1,71 @@
+import { fail } from "@sveltejs/kit"
+
+/** @type {import('./$types').Actions} */
+export const actions = {
+ submitContactUs: async ({ request, locals: { supabaseServiceRole } }) => {
+ const formData = await request.formData()
+ const errors: { [fieldName: string]: string } = {}
+
+ const firstName = formData.get("first_name")?.toString() ?? ""
+ if (firstName.length < 2) {
+ errors["first_name"] = "First name is required"
+ }
+ if (firstName.length > 500) {
+ errors["first_name"] = "First name too long"
+ }
+
+ const lastName = formData.get("last_name")?.toString() ?? ""
+ if (lastName.length < 2) {
+ errors["last_name"] = "Last name is required"
+ }
+ if (lastName.length > 500) {
+ errors["last_name"] = "Last name too long"
+ }
+
+ const email = formData.get("email")?.toString() ?? ""
+ if (email.length < 6) {
+ errors["email"] = "Email is required"
+ } else if (email.length > 500) {
+ errors["email"] = "Email too long"
+ } else if (!email.includes("@") || !email.includes(".")) {
+ errors["email"] = "Invalid email"
+ }
+
+ const company = formData.get("company")?.toString() ?? ""
+ if (company.length > 500) {
+ errors["company"] = "Company too long"
+ }
+
+ const phone = formData.get("phone")?.toString() ?? ""
+ if (phone.length > 100) {
+ errors["phone"] = "Phone number too long"
+ }
+
+ const message = formData.get("message")?.toString() ?? ""
+ if (message.length > 2000) {
+ errors["message"] = "Message too long (" + message.length + " of 2000)"
+ }
+
+ console.log("errors:", errors)
+ if (Object.keys(errors).length > 0) {
+ return fail(400, { errors })
+ }
+
+ // Save to database
+ const { error: insertError } = await supabaseServiceRole
+ .from("contact_requests")
+ .insert({
+ first_name: firstName,
+ last_name: lastName,
+ email,
+ company_name: company,
+ phone,
+ message_body: message,
+ updated_at: new Date(),
+ })
+
+ if (insertError) {
+ return fail(500, { errors: { _: "Error saving" } })
+ }
+ },
+}
diff --git a/src/routes/(marketing)/contact_us/+page.svelte b/src/routes/(marketing)/contact_us/+page.svelte
new file mode 100644
index 0000000..195bb21
--- /dev/null
+++ b/src/routes/(marketing)/contact_us/+page.svelte
@@ -0,0 +1,156 @@
+
+
+
+
+
+
Contact Us
+
Talk to one of our service professionals to:
+
+ - Get a live demo
+ - Discuss your specific needs
+ - Get a quote
+ - Answer any technical questions you have
+
+
Once you complete the form, we'll reach out to you! *
+
+ *Not really for this demo page, but you should say something like that
+ 😉
+
+
+
+
+
+ {#if showSuccess}
+
+
+
Thank you!
+
We've received your message and will be in touch soon.
+
+
+ {:else}
+
+ {/if}
+
+
diff --git a/src/routes/(marketing)/login/+layout.server.ts b/src/routes/(marketing)/login/+layout.server.ts
new file mode 100644
index 0000000..e1760f4
--- /dev/null
+++ b/src/routes/(marketing)/login/+layout.server.ts
@@ -0,0 +1,19 @@
+import { redirect } from "@sveltejs/kit"
+import type { LayoutServerLoad } from "./$types"
+
+export const load: LayoutServerLoad = async ({
+ url,
+ locals: { safeGetSession },
+}) => {
+ const { session } = await safeGetSession()
+
+ // if the user is already logged in return them to the account page
+ if (session) {
+ throw redirect(303, "/account")
+ }
+
+ return {
+ session: session,
+ url: url.origin,
+ }
+}
diff --git a/src/routes/(marketing)/login/+layout.svelte b/src/routes/(marketing)/login/+layout.svelte
new file mode 100644
index 0000000..9e5239c
--- /dev/null
+++ b/src/routes/(marketing)/login/+layout.svelte
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+ 🍪 Logging in uses Cookies 🍪
+
+
+
diff --git a/src/routes/(marketing)/login/+layout.ts b/src/routes/(marketing)/login/+layout.ts
new file mode 100644
index 0000000..933af3f
--- /dev/null
+++ b/src/routes/(marketing)/login/+layout.ts
@@ -0,0 +1,41 @@
+import {
+ PUBLIC_SUPABASE_ANON_KEY,
+ PUBLIC_SUPABASE_URL,
+} from "$env/static/public"
+import {
+ createBrowserClient,
+ createServerClient,
+ isBrowser,
+ parse,
+} from "@supabase/ssr"
+
+export const load = async ({ fetch, data, depends }) => {
+ depends("supabase:auth")
+
+ const supabase = isBrowser()
+ ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
+ global: {
+ fetch,
+ },
+ cookies: {
+ get(key) {
+ const cookie = parse(document.cookie)
+ return cookie[key]
+ },
+ },
+ })
+ : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
+ global: {
+ fetch,
+ },
+ cookies: {
+ get() {
+ return JSON.stringify(data.session)
+ },
+ },
+ })
+
+ const url = data.url
+
+ return { supabase, url }
+}
diff --git a/src/routes/(marketing)/login/+page.svelte b/src/routes/(marketing)/login/+page.svelte
new file mode 100644
index 0000000..adfdec4
--- /dev/null
+++ b/src/routes/(marketing)/login/+page.svelte
@@ -0,0 +1,16 @@
+
+ Log In
+
+
+
diff --git a/src/routes/(marketing)/login/current_password_error/+page.svelte b/src/routes/(marketing)/login/current_password_error/+page.svelte
new file mode 100644
index 0000000..b15e245
--- /dev/null
+++ b/src/routes/(marketing)/login/current_password_error/+page.svelte
@@ -0,0 +1,19 @@
+
+ Current Password Incorrect
+
+
+Current Password Incorrect
+
+
+ You attempted edit your account with an incorrect current password, and have
+ been logged out.
+
+
+ If you remember your password sign in and try again.
+
+
+ If you forget your password reset it.
+
diff --git a/src/routes/(marketing)/login/forgot_password/+page.server.ts b/src/routes/(marketing)/login/forgot_password/+page.server.ts
new file mode 100644
index 0000000..62ad4e4
--- /dev/null
+++ b/src/routes/(marketing)/login/forgot_password/+page.server.ts
@@ -0,0 +1 @@
+export const ssr = false
diff --git a/src/routes/(marketing)/login/forgot_password/+page.svelte b/src/routes/(marketing)/login/forgot_password/+page.svelte
new file mode 100644
index 0000000..a8baa1f
--- /dev/null
+++ b/src/routes/(marketing)/login/forgot_password/+page.svelte
@@ -0,0 +1,26 @@
+
+
+
+ Forgot Password
+
+
+Forgot Password
+
+
+ Remember your password?
Sign in.
+
diff --git a/src/routes/(marketing)/login/login_config.ts b/src/routes/(marketing)/login/login_config.ts
new file mode 100644
index 0000000..76ea58c
--- /dev/null
+++ b/src/routes/(marketing)/login/login_config.ts
@@ -0,0 +1,30 @@
+import { ThemeSupa } from "@supabase/auth-ui-shared"
+import type { Provider } from "@supabase/supabase-js"
+
+export const oauthProviders = ["github"] as Provider[]
+
+// use the css variables from DaisyUI to style Supabase auth template
+export const sharedAppearance = {
+ theme: ThemeSupa,
+ variables: {
+ default: {
+ colors: {
+ brand: "oklch(var(--p))",
+ brandAccent: "oklch(var(--ac))",
+ inputText: "oklch(var(--n))",
+ brandButtonText: "oklch(var(--pc))",
+ messageText: "oklch(var(--b))",
+ dividerBackground: "oklch(var(--n))",
+ inputLabelText: "oklch(var(--n))",
+ defaultButtonText: "oklch(var(--n))",
+ anchorTextColor: "oklch(var(--p))",
+ },
+ fontSizes: {
+ baseInputSize: "16px",
+ },
+ },
+ },
+ className: {
+ button: "authBtn",
+ },
+}
diff --git a/src/routes/(marketing)/login/sign_in/+page.server.ts b/src/routes/(marketing)/login/sign_in/+page.server.ts
new file mode 100644
index 0000000..62ad4e4
--- /dev/null
+++ b/src/routes/(marketing)/login/sign_in/+page.server.ts
@@ -0,0 +1 @@
+export const ssr = false
diff --git a/src/routes/(marketing)/login/sign_in/+page.svelte b/src/routes/(marketing)/login/sign_in/+page.svelte
new file mode 100644
index 0000000..ef3ef70
--- /dev/null
+++ b/src/routes/(marketing)/login/sign_in/+page.svelte
@@ -0,0 +1,63 @@
+
+
+
+ Sign in
+
+
+{#if $page.url.searchParams.get("verified") == "true"}
+
+
+
Email verified! Please sign in.
+
+{/if}
+Sign In
+
+
+
+ Don't have an account?
Sign up.
+
diff --git a/src/routes/(marketing)/login/sign_up/+page.server.ts b/src/routes/(marketing)/login/sign_up/+page.server.ts
new file mode 100644
index 0000000..62ad4e4
--- /dev/null
+++ b/src/routes/(marketing)/login/sign_up/+page.server.ts
@@ -0,0 +1 @@
+export const ssr = false
diff --git a/src/routes/(marketing)/login/sign_up/+page.svelte b/src/routes/(marketing)/login/sign_up/+page.svelte
new file mode 100644
index 0000000..15b9c51
--- /dev/null
+++ b/src/routes/(marketing)/login/sign_up/+page.svelte
@@ -0,0 +1,25 @@
+
+
+
+ Sign up
+
+
+Sign Up
+
+
+ Have an account?
Sign in.
+
diff --git a/src/routes/(marketing)/pricing/+page.svelte b/src/routes/(marketing)/pricing/+page.svelte
new file mode 100644
index 0000000..0d3ba51
--- /dev/null
+++ b/src/routes/(marketing)/pricing/+page.svelte
@@ -0,0 +1,215 @@
+
+
+
+ Pricing
+
+
+
+
+
Pricing
+
+ Totally free, scale to millions of users
+
+
+
+
+
Pricing FAQ
+
+
+
+
+
+ Is this template free to use?
+
+
+
Yup! This template is free to use for any project.
+
+
+
+
+
+ Why does a free template have a pricing page?
+
+
+
+ The pricing page is part of the boilerplate. It shows how the
+ pricing page integrates into the billing portal and the Stripe
+ Checkout flows.
+
+
+
+
+
+
+ What license is the template under?
+
+
+
The template is under the MIT license.
+
+
+
+
+
+ Can I try out purchase flows without real a credit card?
+
+
+
+ Our demo page SaasStarter.work has a functional demo page, using Stripe's test environment.
+
+
+ You can use the credit card number 4242 4242 4242 4242 with any
+ future expiry date to test the payment and upgrade flows.
+
+
+
+
+
+
+
+
+
+
+
Plan Features
+
+ Example feature table
+
+
+
+
+
+
+ |
+ Free |
+ Pro |
+
+
+
+ {#each planFeatures as feature}
+ {#if feature.header}
+
+ {feature.name} |
+
+ {:else}
+
+ {feature.name} |
+
+ {#if feature.freeString}
+ {feature.freeString}
+ {:else if feature.freeIncluded}
+
+ {:else}
+
+ {/if}
+ |
+
+ {#if feature.proString}
+ {feature.proString}
+ {:else if feature.proIncluded}
+
+ {:else}
+
+ {/if}
+ |
+
+ {/if}
+ {/each}
+
+
+
+
+
diff --git a/src/routes/(marketing)/pricing/+page.ts b/src/routes/(marketing)/pricing/+page.ts
new file mode 100644
index 0000000..176ae64
--- /dev/null
+++ b/src/routes/(marketing)/pricing/+page.ts
@@ -0,0 +1 @@
+export const prerender = true
diff --git a/src/routes/(marketing)/pricing/pricing_module.svelte b/src/routes/(marketing)/pricing/pricing_module.svelte
new file mode 100644
index 0000000..5cb25ec
--- /dev/null
+++ b/src/routes/(marketing)/pricing/pricing_module.svelte
@@ -0,0 +1,60 @@
+
+
+
+ {#each pricingPlans as plan}
+
+
+
{plan.name}
+
+ {plan.description}
+
+
+ Plan Includes:
+
+ {#each plan.features as feature}
+ - {feature}
+ {/each}
+
+
+
+
+
{plan.price}
+
{plan.priceIntervalName}
+
+ {#if plan.id === currentPlanId}
+
+ Current Plan
+
+ {:else}
+
+ {callToAction}
+
+ {/if}
+
+
+
+
+ {/each}
+
diff --git a/src/routes/(marketing)/pricing/pricing_plans.ts b/src/routes/(marketing)/pricing/pricing_plans.ts
new file mode 100644
index 0000000..a58b281
--- /dev/null
+++ b/src/routes/(marketing)/pricing/pricing_plans.ts
@@ -0,0 +1,43 @@
+export const defaultPlanId = "free"
+
+export const pricingPlans = [
+ {
+ id: "free",
+ name: "Free",
+ description: "A free plan to get you started!",
+ price: "$0",
+ priceIntervalName: "per month",
+ stripe_price_id: null,
+ features: ["MIT Licence", "Fast Performance", "Stripe Integration"],
+ },
+ {
+ id: "pro",
+ name: "Pro",
+ description:
+ "A plan to test the purchase experience. Try buying this with the test credit card 4242424242424242.",
+ price: "$5",
+ priceIntervalName: "per month",
+ stripe_price_id: "price_1NkdZCHMjzZ8mGZnRSjUm4yA",
+ stripe_product_id: "prod_OXj1CcemGMWOlU",
+ features: [
+ "Everything in Free",
+ "Support us with fake money",
+ "Test the purchase experience",
+ ],
+ },
+ {
+ id: "enterprise",
+ name: "Enterprise",
+ description:
+ "A plan to test the upgrade expereince. Try buying this with the test credit card 4242424242424242.",
+ price: "$15",
+ priceIntervalName: "per month",
+ stripe_price_id: "price_1Nkda2HMjzZ8mGZn4sKvbDAV",
+ stripe_product_id: "prod_OXj20YNpHYOXi7",
+ features: [
+ "Everything in Pro",
+ "Try the 'upgrade plan' UX",
+ "Still actually free!",
+ ],
+ },
+]
diff --git a/src/routes/+error.svelte b/src/routes/+error.svelte
new file mode 100644
index 0000000..872198c
--- /dev/null
+++ b/src/routes/+error.svelte
@@ -0,0 +1,16 @@
+
+
+
+
+
+
This is embarassing...
+
There was an error: {$page?.error?.message}
+
+
+
+
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
new file mode 100644
index 0000000..a9e8d25
--- /dev/null
+++ b/src/routes/+layout.svelte
@@ -0,0 +1,21 @@
+
+
+{#if $navigating}
+
+
+{/if}
+
diff --git a/static/favicon.png b/static/favicon.png
new file mode 100644
index 0000000..7841249
Binary files /dev/null and b/static/favicon.png differ
diff --git a/static/images/cm_logo.svg b/static/images/cm_logo.svg
new file mode 100644
index 0000000..bae2a82
--- /dev/null
+++ b/static/images/cm_logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/images/example-home.png b/static/images/example-home.png
new file mode 100644
index 0000000..ff2e9f3
Binary files /dev/null and b/static/images/example-home.png differ
diff --git a/static/images/rss.svg b/static/images/rss.svg
new file mode 100644
index 0000000..14a6f3f
--- /dev/null
+++ b/static/images/rss.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/robots.txt b/static/robots.txt
new file mode 100644
index 0000000..4738d6a
--- /dev/null
+++ b/static/robots.txt
@@ -0,0 +1,3 @@
+User-agent: *
+Disallow:
+
diff --git a/svelte.config.js b/svelte.config.js
new file mode 100644
index 0000000..0e3266d
--- /dev/null
+++ b/svelte.config.js
@@ -0,0 +1,15 @@
+import adapter from "@sveltejs/adapter-auto"
+import { vitePreprocess } from "@sveltejs/kit/vite"
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ kit: {
+ // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
+ // If your environment is not supported or you settled on a specific environment, switch out the adapter.
+ // See https://kit.svelte.dev/docs/adapters for more information about adapters.
+ adapter: adapter(),
+ },
+ preprocess: vitePreprocess(),
+}
+
+export default config
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..01c6a74
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,28 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ["./src/**/*.{html,js,svelte,ts}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [require("@tailwindcss/typography"), require("daisyui")],
+ daisyui: {
+ themes: [
+ {
+ saasstartertheme: {
+ primary: "#180042",
+ "primary-content": "#fefbf6",
+ secondary: "#c7b9f8",
+ neutral: "#180042",
+ "neutral-content": "#fefbf6",
+ accent: "#db2777",
+ "accent-content": "#180042",
+ "base-content": "#180042",
+ "base-100": "#fefbf6",
+ "base-200": "#faedd6",
+ success: "#37d399",
+ error: "#f77272",
+ },
+ },
+ ],
+ },
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..b987159
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ },
+ // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias and https://kit.svelte.dev/docs/configuration#files
+ //
+ // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
+ // from the referenced tsconfig.json - TypeScript does not merge them in
+}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..d1d54a1
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,9 @@
+import { sveltekit } from "@sveltejs/kit/vite"
+import { defineConfig } from "vitest/config"
+
+export default defineConfig({
+ plugins: [sveltekit()],
+ test: {
+ include: ["src/**/*.{test,spec}.{js,ts}"],
+ },
+})