feat: /contact page on classroomio-com (#428)

* contact page v1

* finished the UI of the contact us page

* working on the forms

* All functions and logic done

* some refactor

* some refactor

* some refactor

* some refactor

* added loading icon

* removed the image

* finished the send email

* some cleanup

* chore: refactoring

* finishing touches

* update .env.example

---------

Co-authored-by: emmanuel <chifez1@gmail.com>
This commit is contained in:
Rotimi Best
2024-11-12 02:18:51 +01:00
committed by GitHub
co-authored by emmanuel
parent b78a0acc99
commit 89569fe6d1
18 changed files with 862 additions and 185 deletions
@@ -0,0 +1,22 @@
<script>
import Renew from 'carbon-icons-svelte/lib/Renew.svelte';
export let label = '';
export let className = '';
export let isLoading = false;
export let isDisabled = false;
export let onClick = () => {};
</script>
<div class="w-full flex items-center justify-center pt-5">
<button
class="bg-[#0233BD] text-white font-medium text-sm py-4 {className}"
disabled={isDisabled}
on:click={onClick}
>
{#if isLoading}
<Renew size={24} class="animate-spin" />
{:else}
{label}
{/if}
</button>
</div>
@@ -0,0 +1,70 @@
<script>
import Button from '$lib/Button/Button.svelte';
import TextArea from '$lib/Input/TextArea.svelte';
import TextField from '$lib/Input/TextField.svelte';
import { isFormValid } from '$lib/utils/isFormValid';
import { submitForm } from '$lib/utils/submitForm';
import Sucess from '$lib/Contact/Sucess.svelte';
let name = '';
let email = '';
let title = '';
let description = '';
let isLoading = false;
let errorMessage = '';
let sent = false;
const handleClick = async () => {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[FEEDBACK] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
};
</script>
<div>
{#if sent}
<Sucess />
{:else}
<div class="w-full my-10 space-y-5">
<div class="flex flex-col md:flex-row gap-10">
<TextField label="Your Name" isRequired className="w-full" bind:value={name} />
<TextField label="Your Email" isRequired className="w-full" bind:value={email} />
</div>
<TextField label="Subject" isRequired bind:value={title} />
<TextArea label="Message" isRequired bind:value={description} />
{#if !!errorMessage}
<p class="text-left text-sm text-red-500">{errorMessage}</p>
{/if}
<Button
label="Submit"
className="px-32"
onClick={handleClick}
isDisabled={isLoading}
{isLoading}
/>
</div>
{/if}
</div>
@@ -0,0 +1,70 @@
<script>
import Button from '$lib/Button/Button.svelte';
import TextArea from '$lib/Input/TextArea.svelte';
import TextField from '$lib/Input/TextField.svelte';
import { isFormValid } from '$lib/utils/isFormValid';
import { submitForm } from '$lib/utils/submitForm';
import Sucess from './Sucess.svelte';
let name = '';
let email = '';
let title = '';
let description = '';
let isLoading = false;
let errorMessage = '';
let sent = false;
async function handleClick() {
const data = { name, email, title, description };
if (!isFormValid(data)) {
errorMessage = 'Please fill out all required fields.';
return;
}
isLoading = true;
const formData = {
title: `[HELP] ${title}`,
description: `My name is ${JSON.stringify(name)}, email is ${JSON.stringify(
email
)}. \n\n${JSON.stringify(description)}.`,
replyTo: email
};
try {
await submitForm(formData);
sent = true;
} catch (error) {
console.log(error);
} finally {
isLoading = false;
}
}
</script>
<div>
{#if sent}
<Sucess />
{:else}
<div class="w-full my-10 space-y-5">
<div class="flex flex-col md:flex-row gap-10">
<TextField label="Your Name" isRequired className="w-full" bind:value={name} />
<TextField label="Your Email" isRequired className="w-full" bind:value={email} />
</div>
<TextField label="Subject" isRequired bind:value={title} />
<TextArea label="Message" isRequired bind:value={description} />
{#if !!errorMessage}
<p class="text-left text-sm text-red-500">{errorMessage}</p>
{/if}
<Button
label="Submit"
className="px-32"
onClick={handleClick}
isDisabled={isLoading}
{isLoading}
/>
</div>
{/if}
</div>
@@ -0,0 +1,17 @@
<script>
import { goto } from '$app/navigation';
import Button from '$lib/Button/Button.svelte';
</script>
<div
class="relative overflow-hidden mt-10 flex space-y-3 flex-col text-center items-center border border-[#D9E0F5] justify-center w-full rounded-md min-h-[300px] p-4 bg-[#F5F8FE]"
>
<div class="absolute w-20 h-20 bg-[#0233BD] -top-14 -left-9 rotate-[50deg]" />
<h1 class="font-bold text-3xl">Your message has been sent 🎉</h1>
<p class="w-[70%] font-normal text-sm">
We'll get back to you as soon as possible. In the meantime, Explore and enjoy features on our
website
</p>
<Button label="Go to Home" className="px-10 md:px-20" onClick={() => goto('/')} />
</div>
@@ -98,6 +98,11 @@
Resources
</p>
<ul class="space-y-2 lg:space-y-4">
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
<a href="/contact">Contact us</a>
</li>
<li
class="lg:text-base text-sm leading-6 tracking-wide text-gray-600 hover:text-gray-900"
>
@@ -130,8 +130,9 @@
<a
href="/#customization"
class="px-6 py-2 border-[1.5px] border-[#0233BD] rounded-[4px] bg-white text-[#0233BD] text-lg font-bold"
>Customizable LMS</a
>
Customizable LMS
</a>
<svg
class="absolute -right-1 -bottom-2.5"
@@ -0,0 +1,56 @@
<script lang="ts">
export let label = '';
export let disabled = false;
export let placeholder = '';
export let value = '';
export let rows = 3;
export let className = '';
export let labelClassName = '';
export let bgColor = 'bg-gray-50 focus:bg-primary-50 dark:bg-neutral-700 dark:text-white';
export let helperMessage = '';
export let errorMessage = '';
export let isRequired = false;
export let onChange = () => {}; // This is to know if element is 'dirty'
export let ref = null;
</script>
<label for="text-field" class="block w-full relative {className}">
{#if label}
<p class="dark:text-white text-left w-full flex items-center justify-between {labelClassName}">
<span>
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</span>
</p>
{/if}
<textarea
{disabled}
{rows}
{placeholder}
bind:value
bind:this={ref}
required={isRequired}
class="form-textarea p-2 border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 dark:border-neutral-600 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 focus:border-primary-600 mt-1 block w-full
{bgColor} {!!errorMessage ? 'border-red-500' : 'border-gray-300'}"
on:change={onChange}
/>
{#if !!errorMessage}
<p class="text-sm text-red-500">
{errorMessage}
</p>
{:else if helperMessage}
<p class="text-sm text-gray-500 dark:text-white">
{helperMessage}
</p>
{/if}
<slot name="iconbutton" />
</label>
<style>
.form-textarea {
box-shadow: none !important;
}
</style>
@@ -0,0 +1,81 @@
<script lang="ts">
import { onMount } from 'svelte';
export let label = '';
export let placeholder = '';
export let value: string | number | null = null;
export let name = '';
export let className = '';
export let inputClassName = '';
export let labelClassName = '';
export let bgColor = 'bg-gray-100 focus:bg-primary-50 dark:bg-neutral-700 dark:text-white';
export let autoFocus = false;
export let isRequired = false;
export let isDisabled = false;
export let errorMessage = '';
export let helperMessage = '';
export let autoComplete = true;
let ref: HTMLInputElement | undefined = undefined;
let focusClass = '';
onMount(() => {
if (autoFocus && ref) {
ref.focus();
}
});
</script>
<label class="block relative {className}">
{#if label}
<p class=" dark:text-white text-left m-0 {labelClassName}">
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</p>
{/if}
<input
class="form-input border-l-0 border-r-0 border-t-0 border-b-2 border-gray-200 dark:border-neutral-600 focus:border-l-0 focus:border-r-0 rounded-t-md focus:border-t-0 focus:border-b-2 {inputClassName} {focusClass} {isDisabled &&
'hover:cursor-not-allowed opacity-50'} dark:text-black p-2 mt-1 block w-full {bgColor} {errorMessage
? 'border-red-600'
: ''}"
{placeholder}
bind:value
bind:this={ref}
required={isRequired}
disabled={isDisabled}
autocomplete={autoComplete ? 'on' : 'off'}
on:blur={(e) => {
if (focusClass.includes('border-primary-600')) {
focusClass = focusClass.replace('border-primary-600', '');
}
}}
on:focus={() => (focusClass += ' border-primary-600')}
{name}
/>
{#if errorMessage}
<p class="text-sm text-red-500">{errorMessage}</p>
{:else if helperMessage}
<p class="dark:text-white text-sm">
{helperMessage}
</p>
{/if}
</label>
<style>
.form-input {
box-shadow: none !important;
}
.password-eye {
position: absolute;
top: 30px;
right: 6px;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
</style>
@@ -3,7 +3,7 @@
</script>
<header
class="hero-bg lg:min-h-[30rem] mt-[10%] md:mt-16 pt-24 pb-16 w-full border-b border-slate-100 {className}"
class="hero-bg lg:min-h-[20rem] mt-[10%] md:mt-16 pt-24 pb-16 w-full border-b border-slate-100 {className}"
>
<slot />
</header>
@@ -0,0 +1,76 @@
<script lang="ts">
import ImageCopy from 'carbon-icons-svelte/lib/ImageCopy.svelte';
import TrashCan from 'carbon-icons-svelte/lib/TrashCan.svelte';
export let imageURL = '';
export let label = '';
export let isRequired = false;
export let labelClassName = '';
let isUploading = false;
let fileInput: HTMLInputElement;
const onFileSelected = () => {
const file = fileInput?.files?.[0];
const sizeInkb = file?.size! / 1024;
if (sizeInkb > 500) {
return;
}
if (file) {
let reader = new FileReader();
reader.readAsDataURL(file);
reader.onloadend = () => {
imageURL = reader.result as string;
};
}
};
const deleteImage = () => {
imageURL = '';
if (fileInput) {
fileInput.value = '';
}
};
</script>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div>
{#if label}
<p class="dark:text-white text-left w-full flex items-center justify-between {labelClassName}">
<span>
{label}
{#if isRequired}
<span class="text-red-700">*</span>
{/if}
</span>
{#if imageURL}
<span title="clear image" on:click={deleteImage}>
<TrashCan class="fill-red-500 " />
</span>
{/if}
</p>
{/if}
<div
class="flex items-center justify-center w-full border rounded-md min-h-[200px] p-4"
on:click={() => fileInput.click()}
>
<input
type="file"
style="display: none;"
bind:this={fileInput}
on:change={onFileSelected}
disabled={isUploading}
required={isRequired}
/>
{#if imageURL}
<img src={imageURL} alt="selected file" class="w-[30%] h-auto mt-4" />
{:else}
<div class="space-y-4">
<ImageCopy size={24} class="mx-auto" />
<p class="text-center text-sm text-gray-500 my-2">
Max file size: 10MB, accepted: jpeg, jpg, png, gif
</p>
</div>
{/if}
</div>
</div>
+9
View File
@@ -19,3 +19,12 @@ export type OssFriend = {
description: string;
href: string;
};
export type FORM_TYPE_KEY = keyof typeof FORM_TYPE;
export enum FORM_TYPE {
BUG = 'BUG',
HELP = 'HELP',
FEATURE = 'FEATURE',
FEEDBACK = 'FEEDBACK'
}
@@ -0,0 +1,8 @@
export const isFormValid = (data: { [x: string]: any }) => {
for (const key in data) {
if (!data[key]) {
return false;
}
}
return true;
};
@@ -0,0 +1,20 @@
export const submitForm = async (formData: any) => {
try {
const response = await fetch('/api/contact', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
});
if (!response.ok) {
throw new Error('Failed to submit the form');
}
const data = await response.json();
console.log('Response from API:', data);
} catch (error) {
console.error('Error submitting form:', error);
}
};
@@ -0,0 +1,65 @@
import { PUBLIC_SERVER_URL } from '$env/static/public';
const sendEmail = async (subject: string, content: string, replyTo?: string) => {
try {
const response = await fetch(`${PUBLIC_SERVER_URL}/sendEmail`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify([
{
from: `"ClassroomIO Support" <notify@mail.classroomio.com>`,
to: 'help@classroomio.com',
subject,
content,
replyTo
}
])
});
const responseData = await response.json();
console.log('Emails sent successfully:', responseData);
return 'sent';
} catch (error) {
console.error('Error sending email', error);
return 'failed';
}
};
export async function POST({ request }) {
try {
const { title, description, replyTo } = await request.json();
console.log(title, description, replyTo);
const result = await sendEmail(title, description, replyTo);
return new Response(
JSON.stringify({
message: 'Issue created successfully',
issue: result
}),
{
status: 201,
headers: {
'Content-Type': 'application/json'
}
}
);
} catch (error) {
console.log('errors from backend', error);
return new Response(
JSON.stringify({
message: 'Failed to create issue',
error: (error as Error)?.message
}),
{
status: 500,
headers: {
'Content-Type': 'application/json'
}
}
);
}
}
@@ -0,0 +1 @@
export const prerender = true;
@@ -0,0 +1,106 @@
<script lang="ts">
import ArrowUpRight from 'carbon-icons-svelte/lib/ArrowUpRight.svelte';
import PageHeader from '$lib/PageHeader/PageHeader.svelte';
import FeedbackForm from '$lib/Contact/FeedbackForm.svelte';
import HelpForm from '$lib/Contact/HelpForm.svelte';
import { FORM_TYPE } from '$lib/types';
import type { FORM_TYPE_KEY } from '$lib/types';
let currentForm: FORM_TYPE_KEY;
const formList = [
{
id: FORM_TYPE.BUG,
title: '🐜 I want to report a bug',
to: 'https://github.com/classroomio/classroomio/issues/new?assignees=&labels=bug&projects=&template=bug-report.yml'
},
{
id: FORM_TYPE.HELP,
title: '🙋‍♂️ I need help'
},
{
id: FORM_TYPE.FEATURE,
title: '📮 I want to request a feature',
to: 'https://github.com/classroomio/classroomio/issues/new?assignees=&labels=&projects=&template=feature-request.yml'
},
{
id: FORM_TYPE.FEEDBACK,
title: '💭 I have feedback'
}
];
const onChange = (form: FORM_TYPE_KEY, to?: string) => {
currentForm = form;
if (to) {
window.open(to, '_blank');
}
};
</script>
<svelte:head>
<title>Contact us | ClassroomIO</title>
</svelte:head>
<section>
<PageHeader className="flex flex-col items-center justify-center bg-[#F5F8FE] text-center">
<div class="mb-2 ml-[5%] lg:ml-0 flex w-full items-center justify-center">
<span
style="color: rgb(75, 85, 99);"
class="rounded-full border-2 border-[#C2D2FF] px-4 py-1 text-sm font-medium bg-[#DCE5FF] text-blue-700"
>
Contact Us
</span>
</div>
<h1
class="mx-auto text-4xl md:text-7xl lg:text-6xl font-bold text-slate-900 flex flex-col items-center"
>
<span>Get in touch with <span class="text-blue-700">Us</span></span>
</h1>
<p
class="w-[90%] md:w-[60%] max-w-3xl text-center font-normal text-lg text-slate-700 mt-10 lg:mt-7"
>
Report any bugs you encounter, request new features you'd like to see, or ask any questions
you have about ClassroomIO
</p>
</PageHeader>
<div class="my-20 mx-auto">
<!-- Content here -->
<div
class="mx-auto w-[90%] lg:w-[60%] place-items-center grid grid-cols-1 md:grid-cols-2 gap-10"
>
{#each formList as list}
<button
on:click={() => onChange(list.id, list.to)}
class="text-start border-[1.5px] w-full rounded-md p-3 {currentForm === list.id
? `border-[#0233BD]`
: `border-gray-200`} flex items-center justify-between"
>
<p class="text-lg text-slate-700 font-normal">{list.title}</p>
{#if list.to}
<ArrowUpRight size={16} />
{/if}
</button>
{/each}
</div>
{#if currentForm === FORM_TYPE[currentForm]}
<div class="mx-auto w-[90%] lg:w-[60%] px-5">
{#if currentForm === FORM_TYPE.HELP}
<HelpForm />
{:else if currentForm === FORM_TYPE.FEEDBACK}
<FeedbackForm />
{/if}
</div>
{/if}
<!-- <PageSignupCTA
header="Scale your Teaching Business in Minutes"
subText="Don't wait, let's get you started."
btnLabel="Register"
link="/teach/register"
demo={false}
/> -->
</div>
</section>