mirror of
https://github.com/jcreek/SvelteKitSaasBoilerplate.git
synced 2026-07-15 12:03:49 +00:00
Merge pull request #94 from jcreek/79-create-contact-page
feat(#79): Create contact page
This commit is contained in:
@@ -12,3 +12,5 @@ VITE_BREVO_API_KEY=""
|
|||||||
VITE_BREVO_SENDER_EMAIL="noreply@example.com"
|
VITE_BREVO_SENDER_EMAIL="noreply@example.com"
|
||||||
# Display name for the sender email
|
# Display name for the sender email
|
||||||
VITE_BREVO_SENDER_NAME="No Reply"
|
VITE_BREVO_SENDER_NAME="No Reply"
|
||||||
|
# Contact email for the contact page form to send to
|
||||||
|
VITE_CONTACT_EMAIL=""
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
type RateLimitStore = {
|
||||||
|
[key: string]: { count: number; lastRequest: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
const rateLimitStore: RateLimitStore = {};
|
||||||
|
const RATE_LIMIT_WINDOW = 60 * 5000; // 5 minutes
|
||||||
|
const MAX_REQUESTS = 5;
|
||||||
|
|
||||||
|
export function isRateLimited(ip: string): boolean {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
const requestInfo = rateLimitStore[ip];
|
||||||
|
|
||||||
|
if (!requestInfo) {
|
||||||
|
rateLimitStore[ip] = { count: 1, lastRequest: currentTime };
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTime - requestInfo.lastRequest > RATE_LIMIT_WINDOW) {
|
||||||
|
rateLimitStore[ip] = { count: 1, lastRequest: currentTime };
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestInfo.count >= MAX_REQUESTS) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
rateLimitStore[ip].count += 1;
|
||||||
|
rateLimitStore[ip].lastRequest = currentTime;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -184,12 +184,11 @@
|
|||||||
<a href="/contact" class="btn btn-outline w-full">Provide Feedback</a>
|
<a href="/contact" class="btn btn-outline w-full">Provide Feedback</a>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<form method="post" action="?/delete" use:enhance={handleAccountDeletion}>
|
<form method="post" action="?/delete" use:enhance={handleAccountDeletion}>
|
||||||
<div>
|
<div>
|
||||||
<button class="button block" disabled={loading}>Delete My Account</button>
|
<button class="button block" disabled={loading}>Delete My Account</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import type { Actions } from '@sveltejs/kit';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
import { sendEmail } from '$lib/utils/brevo/email';
|
||||||
|
import { VITE_CONTACT_EMAIL } from '$env/static/private';
|
||||||
|
import logger from '$lib/utils/logger/logger';
|
||||||
|
import { isRateLimited } from '$lib/utils/rateLimiter';
|
||||||
|
|
||||||
|
export const actions: Actions = {
|
||||||
|
default: async ({ request }) => {
|
||||||
|
const ip =
|
||||||
|
request.headers.get('x-forwarded-for') ?? request.headers.get('remote-addr') ?? 'unknown';
|
||||||
|
|
||||||
|
if (isRateLimited(ip)) {
|
||||||
|
return fail(429, { error: 'Too many requests. Please try again later.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const formData = await request.formData();
|
||||||
|
const name = formData.get('name') as string;
|
||||||
|
const email = formData.get('email') as string;
|
||||||
|
const message = formData.get('message') as string;
|
||||||
|
|
||||||
|
const emailContent = `
|
||||||
|
<p>You have a new contact form submission:</p>
|
||||||
|
<p><strong>Name:</strong> ${name}</p>
|
||||||
|
<p><strong>Email:</strong> ${email}</p>
|
||||||
|
<p><strong>Message:</strong></p>
|
||||||
|
<p>${message}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sendEmail('New Contact Form Submission', emailContent, VITE_CONTACT_EMAIL);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Failed to send email:', error);
|
||||||
|
return fail(400, { error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import type { SubmitFunction } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
let name = '';
|
||||||
|
let email = '';
|
||||||
|
let message = '';
|
||||||
|
let submitted = false;
|
||||||
|
let error = '';
|
||||||
|
|
||||||
|
const handleSubmit: SubmitFunction = () => {
|
||||||
|
let loading = false;
|
||||||
|
return async ({ result, update }) => {
|
||||||
|
loading = true;
|
||||||
|
if (result.data && result.data.success) {
|
||||||
|
submitted = true;
|
||||||
|
} else {
|
||||||
|
error = result.error?.message || 'Form submission failed';
|
||||||
|
}
|
||||||
|
loading = false;
|
||||||
|
await update();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if submitted}
|
||||||
|
<div class="alert alert-success shadow-lg">
|
||||||
|
<div>
|
||||||
|
<span>Thank you for your message, {name}!</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<form
|
||||||
|
method="post"
|
||||||
|
action="?"
|
||||||
|
use:enhance={handleSubmit}
|
||||||
|
class="max-w-lg mx-auto p-4 bg-base-200 rounded-lg shadow-md"
|
||||||
|
aria-labelledby="contact-form-title"
|
||||||
|
>
|
||||||
|
<h2 id="contact-form-title" class="text-xl font-bold mb-4">Contact Us</h2>
|
||||||
|
{#if error}
|
||||||
|
<div class="alert alert-error shadow-lg mb-4">
|
||||||
|
<div>
|
||||||
|
<span>{error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label for="name" class="label">
|
||||||
|
<span class="label-text">Name *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
bind:value={name}
|
||||||
|
class="input input-bordered"
|
||||||
|
aria-required="true"
|
||||||
|
minlength="2"
|
||||||
|
maxlength="100"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label for="email" class="label">
|
||||||
|
<span class="label-text">Email *</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
bind:value={email}
|
||||||
|
class="input input-bordered"
|
||||||
|
aria-required="true"
|
||||||
|
minlength="5"
|
||||||
|
maxlength="100"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control mb-4">
|
||||||
|
<label for="message" class="label">
|
||||||
|
<span class="label-text">Message *</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
bind:value={message}
|
||||||
|
class="textarea textarea-bordered"
|
||||||
|
aria-required="true"
|
||||||
|
minlength="10"
|
||||||
|
maxlength="1000"
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
<small class="text-sm mt-1">Maximum 1000 characters</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-control">
|
||||||
|
<button type="submit" class="btn btn-primary">Send</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{/if}
|
||||||
Reference in New Issue
Block a user