Skip to content
This repository was archived by the owner on Nov 29, 2025. It is now read-only.
Merged

Dev #27

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions prisma/migrations/20250528040308_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-- CreateTable
CREATE TABLE "ContactSubmission" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"message" TEXT NOT NULL,
"reason" TEXT NOT NULL,
"ipAddress" TEXT,
"userAgent" TEXT,
"referrer" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ContactSubmission_pkey" PRIMARY KEY ("id")
);
17 changes: 17 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -668,3 +668,20 @@ model NewsletterSubscriber {
@@index([isActive])
@@index([subscribedAt])
}



model ContactSubmission {
id String @id @default(uuid())
name String
email String
message String
reason String

// Tracking fields
ipAddress String?
userAgent String?
referrer String?

createdAt DateTime @default(now())
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script>
import '../../../app.css'
import '../../app.css'
import { Menu, Bell, User, Search, FileText, Settings, ChartBar, Home, X, LogOut } from '@lucide/svelte';

/** @type {{ data: import('./$types').LayoutData, children: import('svelte').Snippet }} */
/** @type {{ data: import('./admin/$types').LayoutData, children: import('svelte').Snippet }} */
let { data, children } = $props();

let mobileMenuOpen = $state(false);
Expand Down Expand Up @@ -31,6 +31,10 @@
<FileText class="w-4 h-4 mr-2" />
Blog Posts
</a>
<a href="/admin/contacts" class="flex items-center px-3 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-blue-600 transition-colors">
<User class="w-4 h-4 mr-2" />
Contact Submissions
</a>
<a href="/admin/newsletter" class="flex items-center px-3 py-2 text-sm font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-blue-600 transition-colors">
<ChartBar class="w-4 h-4 mr-2" />
Newsletter
Expand Down Expand Up @@ -76,6 +80,10 @@
<FileText class="w-5 h-5 mr-3" />
Blog Posts
</a>
<a href="/admin/contacts" class="flex items-center px-3 py-2 text-base font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-blue-600 transition-colors" onclick={() => mobileMenuOpen = false}>
<User class="w-5 h-5 mr-3" />
Contact Submissions
</a>
<a href="/admin/analytics" class="flex items-center px-3 py-2 text-base font-medium text-gray-700 rounded-md hover:bg-gray-100 hover:text-blue-600 transition-colors" onclick={() => mobileMenuOpen = false}>
<ChartBar class="w-5 h-5 mr-3" />
Analytics
Expand Down
6 changes: 6 additions & 0 deletions src/routes/(admin)/admin/contacts/+page.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import prisma from '$lib/prisma';
/** @type {import('./$types').PageServerLoad} */
export async function load() {
const contacts = await prisma.contactSubmission.findMany();
return { contacts };
};
97 changes: 97 additions & 0 deletions src/routes/(admin)/admin/contacts/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<script>
/** @type {{ data: import('./$types').PageData }} */
let { data } = $props();

const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
};
</script>

<div class="p-6">
<div class="mb-6">
<h1 class="text-3xl font-bold text-gray-900">Contact Submissions</h1>
</div>

{#if data.contacts && data.contacts.length > 0}
<div class="overflow-x-auto bg-white shadow-lg rounded-lg">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Contact Info
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Reason
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Message
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Submitted
</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
Tracking
</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
{#each data.contacts as contact}
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex flex-col">
<div class="text-sm font-medium text-gray-900">{contact.name}</div>
<div class="text-sm text-gray-500">{contact.email}</div>
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
{contact.reason}
</span>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-900 max-w-xs truncate" title={contact.message}>
{contact.message}
</div>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{formatDate(contact.createdAt)}
</td>
<td class="px-6 py-4">
<div class="text-xs text-gray-500 space-y-1">
{#if contact.ipAddress}
<div>IP: {contact.ipAddress}</div>
{/if}
{#if contact.referrer}
<div class="truncate max-w-32" title={contact.referrer}>
Ref: {contact.referrer}
</div>
{/if}
</div>
</td>
</tr>
{/each}
</tbody>
</table>
</div>

<div class="mt-4 text-sm text-gray-600">
Total submissions: {data.contacts.length}
</div>
{:else}
<div class="text-center py-12">
<div class="mx-auto h-12 w-12 text-gray-400">
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2 2v-5m16 0h-5m-7 0h5"/>
</svg>
</div>
<h3 class="mt-2 text-sm font-medium text-gray-900">No contact submissions</h3>
<p class="mt-1 text-sm text-gray-500">No contact form requests have been submitted yet.</p>
</div>
{/if}
</div>
110 changes: 110 additions & 0 deletions src/routes/(site)/contact/+page.server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import prisma from '$lib/prisma';
import { fail } from '@sveltejs/kit';

/** @type {import('./$types').PageServerLoad} */
export async function load() {
return {};
}

/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {


const data = await request.formData();
const name = data.get('name');
const email = data.get('email');
const serviceType = data.get('serviceType');
const message = data.get('message');

// Server-side validation
const errors = {};

if (!name || name.toString().trim() === '') {
errors.name = 'Name is required';
}

if (!email || email.toString().trim() === '') {
errors.email = 'Email is required';
} else if (!/\S+@\S+\.\S+/.test(email.toString())) {
errors.email = 'Email is invalid';
}

if (!serviceType || serviceType.toString().trim() === '') {
errors.serviceType = 'Please select a service type';
}

if (!message || message.toString().trim() === '') {
errors.message = 'Message is required';
}

if (Object.keys(errors).length > 0) {
return fail(400, {
errors,
name: name?.toString() || '',
email: email?.toString() || '',
serviceType: serviceType?.toString() || '',
message: message?.toString() || ''
});
}

try {
// Get client information from headers
const userAgent = request.headers.get('user-agent');
const forwarded = request.headers.get('x-forwarded-for');
const realIp = request.headers.get('x-real-ip');
const cfConnectingIp = request.headers.get('cf-connecting-ip');
const referrer = request.headers.get('referer');

// Determine IP address (priority: CF > X-Real-IP > X-Forwarded-For)
let ipAddress = cfConnectingIp || realIp;
if (!ipAddress && forwarded) {
ipAddress = forwarded.split(',')[0].trim();
}


// Store submission in database
const submission = await prisma.contactSubmission.create({
data: {
name: name.toString().trim(),
email: email.toString().trim(),
reason: serviceType.toString().trim(),
message: message.toString().trim(),
ipAddress,
userAgent,
referrer
}
});


return {
success: true,
message: 'Thank you for your message! We\'ll get back to you within 24 hours.'
};

} catch (error) {
console.error('Error saving contact submission:', error);

// More specific error handling
if (error.code === 'P1001') {
return fail(500, {
error: 'Database connection failed. Please try again later.',
name: name?.toString() || '',
email: email?.toString() || '',
serviceType: serviceType?.toString() || '',
message: message?.toString() || ''
});
}

return fail(500, {
error: 'Sorry, there was an error submitting your message. Please try again later.',
name: name?.toString() || '',
email: email?.toString() || '',
serviceType: serviceType?.toString() || '',
message: message?.toString() || ''
});
} finally {

}
}
};
Loading