diff --git a/README.md b/README.md index 05d264ae..b8ffac87 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Everything you need to get started for a SaaS company: - User Dashboard with user profile, user settings, update email/password, billing, and more. - Subscriptions powered by Stripe Checkout - Pricing page +- Contact-us form - Billing portal: self serve to change card, upgrade, cancel, or download receipts - Onboarding flow after signup: collect user data, and select a payment plan - Style toolkit: theming and UI components diff --git a/database_migration.sql b/database_migration.sql index 83fdf8b7..d78d6f15 100644 --- a/database_migration.sql +++ b/database_migration.sql @@ -31,6 +31,20 @@ create table stripe_customers ( ); alter table stripe_customers enable row level security; +-- Create a table for "Contact Us" form submissions +-- Limit RLS policies -- only server side access +create table contact_requests ( + id uuid primary key default gen_random_uuid(), + updated_at timestamp with time zone, + first_name text, + last_name text, + email text, + phone text, + company_name text, + message_body text +); +alter table contact_requests enable row level security; + -- This trigger automatically creates a profile entry when a new user signs up via Supabase Auth. -- See https://supabase.com/docs/guides/auth/managing-user-data#using-triggers for more details. create function public.handle_new_user() diff --git a/src/DatabaseDefinitions.ts b/src/DatabaseDefinitions.ts index 5feb52fd..659e8e1f 100644 --- a/src/DatabaseDefinitions.ts +++ b/src/DatabaseDefinitions.ts @@ -9,6 +9,39 @@ export type Json = export interface Database { public: { Tables: { + contact_requests: { + Row: { + company_name: string | null + email: string | null + first_name: string | null + id: string + last_name: string | null + message_body: string | null + phone: string | null + updated_at: Date | null + } + Insert: { + company_name?: string | null + email?: string | null + first_name?: string | null + id?: string + last_name?: string | null + message_body?: string | null + phone?: string | null + updated_at?: Date | null + } + Update: { + company_name?: string | null + email?: string | null + first_name?: string | null + id?: string + last_name?: string | null + message_body?: string | null + phone?: string | null + updated_at?: Date | null + } + Relationships: [] + } profiles: { Row: { avatar_url: string | null diff --git a/src/routes/(marketing)/+layout.svelte b/src/routes/(marketing)/+layout.svelte index 73c798c1..a522c77b 100644 --- a/src/routes/(marketing)/+layout.svelte +++ b/src/routes/(marketing)/+layout.svelte @@ -71,6 +71,7 @@ Overview Pricing Blog + Contact Us Github `, }, + { + name: "Contact Us", + link: "/contact_us", + description: + "Contact form for customers to reach out for demos, quotes, and questions.", + svgContent: ``, + }, { name: "Performance", newPage: true, @@ -228,9 +235,7 @@ class="flex gap-6 mt-12 max-w-[1064px] mx-auto place-content-center flex-wrap" > {#each features as feature} -
+
{ + 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 00000000..500d544d --- /dev/null +++ b/src/routes/(marketing)/contact_us/+page.svelte @@ -0,0 +1,157 @@ + + +
+
+
+

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} +
+
+ {#each formFields as field} + + {/each} + + {#if Object.keys(errors).length > 0} +

+ Please resolve above issues. +

+ {/if} + + +
+
+ {/if} +
+