Skip to content

Commit

Permalink
Merge pull request #20 from CriticalMoments/contact_us
Browse files Browse the repository at this point in the history
Contact Us functionality, including saving to the database
  • Loading branch information
scosman authored Feb 28, 2024
2 parents f051c7b + dbec26e commit fef6d70
Show file tree
Hide file tree
Showing 7 changed files with 285 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions database_migration.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
33 changes: 33 additions & 0 deletions src/DatabaseDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/routes/(marketing)/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
<a class="link link-hover mb-1" href="/">Overview</a>
<a class="link link-hover my-1" href="/pricing">Pricing</a>
<a class="link link-hover my-1" href="/blog">Blog</a>
<a class="link link-hover my-1" href="/contact_us">Contact Us</a>
<a
class="link link-hover my-1"
href="https://github.com/CriticalMoments/CMSaasStarter">Github</a
Expand Down
11 changes: 8 additions & 3 deletions src/routes/(marketing)/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,13 @@
svgContent: `<circle cx="12" cy="12" r="3" stroke="#1C274C" stroke-width="1.5"/>
<path d="M3.66122 10.6392C4.13377 10.9361 4.43782 11.4419 4.43782 11.9999C4.43781 12.558 4.13376 13.0638 3.66122 13.3607C3.33966 13.5627 3.13248 13.7242 2.98508 13.9163C2.66217 14.3372 2.51966 14.869 2.5889 15.3949C2.64082 15.7893 2.87379 16.1928 3.33973 16.9999C3.80568 17.8069 4.03865 18.2104 4.35426 18.4526C4.77508 18.7755 5.30694 18.918 5.83284 18.8488C6.07287 18.8172 6.31628 18.7185 6.65196 18.5411C7.14544 18.2803 7.73558 18.2699 8.21895 18.549C8.70227 18.8281 8.98827 19.3443 9.00912 19.902C9.02332 20.2815 9.05958 20.5417 9.15224 20.7654C9.35523 21.2554 9.74458 21.6448 10.2346 21.8478C10.6022 22 11.0681 22 12 22C12.9319 22 13.3978 22 13.7654 21.8478C14.2554 21.6448 14.6448 21.2554 14.8478 20.7654C14.9404 20.5417 14.9767 20.2815 14.9909 19.9021C15.0117 19.3443 15.2977 18.8281 15.7811 18.549C16.2644 18.27 16.8545 18.2804 17.3479 18.5412C17.6837 18.7186 17.9271 18.8173 18.1671 18.8489C18.693 18.9182 19.2249 18.7756 19.6457 18.4527C19.9613 18.2106 20.1943 17.807 20.6603 17C20.8677 16.6407 21.029 16.3614 21.1486 16.1272M20.3387 13.3608C19.8662 13.0639 19.5622 12.5581 19.5621 12.0001C19.5621 11.442 19.8662 10.9361 20.3387 10.6392C20.6603 10.4372 20.8674 10.2757 21.0148 10.0836C21.3377 9.66278 21.4802 9.13092 21.411 8.60502C21.3591 8.2106 21.1261 7.80708 20.6601 7.00005C20.1942 6.19301 19.9612 5.7895 19.6456 5.54732C19.2248 5.22441 18.6929 5.0819 18.167 5.15113C17.927 5.18274 17.6836 5.2814 17.3479 5.45883C16.8544 5.71964 16.2643 5.73004 15.781 5.45096C15.2977 5.1719 15.0117 4.6557 14.9909 4.09803C14.9767 3.71852 14.9404 3.45835 14.8478 3.23463C14.6448 2.74458 14.2554 2.35523 13.7654 2.15224C13.3978 2 12.9319 2 12 2C11.0681 2 10.6022 2 10.2346 2.15224C9.74458 2.35523 9.35523 2.74458 9.15224 3.23463C9.05958 3.45833 9.02332 3.71848 9.00912 4.09794C8.98826 4.65566 8.70225 5.17191 8.21891 5.45096C7.73557 5.73002 7.14548 5.71959 6.65205 5.4588C6.31633 5.28136 6.0729 5.18269 5.83285 5.15108C5.30695 5.08185 4.77509 5.22436 4.35427 5.54727C4.03866 5.78945 3.80569 6.19297 3.33974 7C3.13231 7.35929 2.97105 7.63859 2.85138 7.87273" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>`,
},
{
name: "Contact Us",
link: "/contact_us",
description:
"Contact form for customers to reach out for demos, quotes, and questions.",
svgContent: `<path d="M10.5 22V20M14.5 22V20" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/><path d="M11 20V20.75H11.75V20H11ZM1.25 12C1.25 12.4142 1.58579 12.75 2 12.75C2.41421 12.75 2.75 12.4142 2.75 12H1.25ZM2.75 16C2.75 15.5858 2.41421 15.25 2 15.25C1.58579 15.25 1.25 15.5858 1.25 16H2.75ZM14 19.25C13.5858 19.25 13.25 19.5858 13.25 20C13.25 20.4142 13.5858 20.75 14 20.75V19.25ZM21.25 11.25C21.25 11.6642 21.5858 12 22 12C22.4142 12 22.75 11.6642 22.75 11.25H21.25ZM17.5 5.25C17.0858 5.25 16.75 5.58579 16.75 6C16.75 6.41421 17.0858 6.75 17.5 6.75V5.25ZM22.75 15C22.75 14.5858 22.4142 14.25 22 14.25C21.5858 14.25 21.25 14.5858 21.25 15H22.75ZM7 5.25C6.58579 5.25 6.25 5.58579 6.25 6C6.25 6.41421 6.58579 6.75 7 6.75V5.25ZM9 19.25C8.58579 19.25 8.25 19.5858 8.25 20C8.25 20.4142 8.58579 20.75 9 20.75V19.25ZM15 20.75C15.4142 20.75 15.75 20.4142 15.75 20C15.75 19.5858 15.4142 19.25 15 19.25V20.75ZM11 19.25H4.23256V20.75H11V19.25ZM4.23256 19.25C3.51806 19.25 2.75 18.5323 2.75 17.3953H1.25C1.25 19.1354 2.48104 20.75 4.23256 20.75V19.25ZM6.5 6.75C8.46677 6.75 10.25 8.65209 10.25 11.25H11.75C11.75 8.04892 9.50379 5.25 6.5 5.25V6.75ZM6.5 5.25C3.49621 5.25 1.25 8.04892 1.25 11.25H2.75C2.75 8.65209 4.53323 6.75 6.5 6.75V5.25ZM10.25 17V20H11.75V17H10.25ZM10.25 11.25V17H11.75V11.25H10.25ZM2.75 12V11.25H1.25V12H2.75ZM2.75 17.3953V16H1.25V17.3953H2.75ZM19.7931 19.25H14V20.75H19.7931V19.25ZM21.25 17.4253C21.25 18.5457 20.4934 19.25 19.7931 19.25V20.75C21.5305 20.75 22.75 19.1488 22.75 17.4253H21.25ZM22.75 11.25C22.75 8.04892 20.5038 5.25 17.5 5.25V6.75C19.4668 6.75 21.25 8.65209 21.25 11.25H22.75ZM21.25 15V17.4253H22.75V15H21.25ZM7 6.75H18V5.25H7V6.75ZM9 20.75H15V19.25H9V20.75Z" fill="#1C274C"/><path d="M5 16H8" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/><path d="M16 9.88432V5.41121M16 5.41121V2.63519C16 2.39905 16.1676 2.19612 16.3994 2.15144L16.8855 2.05779C17.4738 1.94443 18.0821 1.99855 18.6412 2.214L18.7203 2.24451C19.2746 2.4581 19.8807 2.498 20.4582 2.35891C20.7343 2.2924 21 2.50168 21 2.78573V5.00723C21 5.2442 20.8376 5.45031 20.6073 5.5058L20.5407 5.52184C19.9095 5.67387 19.247 5.63026 18.6412 5.39679C18.0821 5.18135 17.4738 5.12722 16.8855 5.24058L16 5.41121Z" stroke="#1C274C" stroke-width="1.5" stroke-linecap="round"/>`,
},
{
name: "Performance",
newPage: true,
Expand Down Expand Up @@ -228,9 +235,7 @@
class="flex gap-6 mt-12 max-w-[1064px] mx-auto place-content-center flex-wrap"
>
{#each features as feature}
<div
class="card bg-white w-[270px] md:w-[300px] min-h-[300px] flex-none shadow-xl"
>
<div class="card bg-white w-[270px] min-h-[300px] flex-none shadow-xl">
<div class="card-body items-center text-center p-[24px] pt-[32px]">
<div>
<svg
Expand Down
71 changes: 71 additions & 0 deletions src/routes/(marketing)/contact_us/+page.server.ts
Original file line number Diff line number Diff line change
@@ -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" } })
}
},
}
157 changes: 157 additions & 0 deletions src/routes/(marketing)/contact_us/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<script lang="ts">
import { enhance, applyAction } from "$app/forms"
import type { SubmitFunction } from "@sveltejs/kit"
let errors: { [fieldName: string]: string } = {}
let loading = false
let showSuccess = false
const formFields = [
{
id: "first_name",
label: "First Name *",
inputType: "text",
autocomplete: "given-name",
},
{
id: "last_name",
label: "Last Name *",
inputType: "text",
autocomplete: "family-name",
},
{
id: "email",
label: "Email *",
inputType: "email",
autocomplete: "email",
},
{
id: "phone",
label: "Phone Number",
inputType: "tel",
autocomplete: "tel",
},
{
id: "company",
label: "Company Name",
inputType: "text",
autocomplete: "organization",
},
{
id: "message",
label: "Message",
inputType: "textarea",
autocomplete: "off",
},
]
const handleSubmit: SubmitFunction = () => {
loading = true
errors = {}
return async ({ update, result }) => {
await update({ reset: false })
await applyAction(result)
loading = false
console.log(result)
if (result.type === "success") {
showSuccess = true
} else if (result.type === "failure") {
errors = result.data?.errors ?? {}
} else if (result.type === "error") {
errors = { _: "An error occurred. Please check inputs and try again." }
}
}
}
</script>

<div
class="flex flex-col lg:flex-row mx-auto my-4 min-h-[70vh] place-items-center lg:place-items-start place-content-center"
>
<div
class="max-w-[400px] lg:max-w-[500px] flex flex-col place-content-center p-4 lg:mr-8 lg:mb-8 lg:min-h-[70vh]"
>
<div class="px-6">
<h1 class="text-2xl lg:text-4xl font-bold mb-4">Contact Us</h1>
<p class="text-lg">Talk to one of our service professionals to:</p>
<ul class="list-disc list-outside pl-6 py-4 space-y-1">
<li class="">Get a live demo</li>
<li class="">Discuss your specific needs</li>
<li>Get a quote</li>
<li>Answer any technical questions you have</li>
</ul>
<p>Once you complete the form, we'll reach out to you! *</p>
<p class="text-sm pt-8">
*Not really for this demo page, but you should say something like that
😉
</p>
</div>
</div>

<div
class="flex flex-col flex-grow m-4 lg:ml-10 min-w-[300px] stdphone:min-w-[360px] max-w-[400px] place-content-center"
>
{#if showSuccess}
<div class="flex flex-col place-content-center lg:min-h-[70vh]">
<div
class="card card-bordered shadow-lg py-6 px-6 mx-2 lg:mx-0 lg:p-6 mb-10"
>
<div class="text-2xl font-bold mb-4">Thank you!</div>
<p class="">We've received your message and will be in touch soon.</p>
</div>
</div>
{:else}
<div class="card card-bordered shadow-lg p-4 pt-6 mx-2 lg:mx-0 lg:p-6">
<form
class="form-widget flex flex-col"
method="POST"
action="?/submitContactUs"
use:enhance={handleSubmit}
>
{#each formFields as field}
<label for={field.id}>
<div class="flex flex-row">
<div class="text-base font-bold">{field.label}</div>
{#if errors[field.id]}
<div class="text-red-600 flex-grow text-sm ml-2 text-right">
{errors[field.id]}
</div>
{/if}
</div>
{#if field.inputType === "textarea"}
<textarea
id={field.id}
name={field.id}
autocomplete={field.autocomplete}
rows={4}
class="{errors[field.id]
? 'input-error'
: ''} h-24 input-sm mt-1 input input-bordered w-full mb-3 text-base py-4"
></textarea>
{:else}
<input
id={field.id}
name={field.id}
type={field.inputType}
autocomplete={field.autocomplete}
class="{errors[field.id]
? 'input-error'
: ''} input-sm mt-1 input input-bordered w-full mb-3 text-base py-4"
/>
{/if}
</label>
{/each}

{#if Object.keys(errors).length > 0}
<p class="text-red-600 text-sm mb-2">
Please resolve above issues.
</p>
{/if}

<button class="btn btn-primary {loading ? 'btn-disabled' : ''}"
>{loading ? "Submitting" : "Submit"}</button
>
</form>
</div>
{/if}
</div>
</div>

0 comments on commit fef6d70

Please sign in to comment.