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

Commit 0f56111

Browse files
committed
feat(contact): implement contact submission handling with server-side validation and enhanced UI
1 parent 35908a0 commit 0f56111

File tree

5 files changed

+358
-124
lines changed

5 files changed

+358
-124
lines changed

src/routes/(admin)/admin/+layout.svelte renamed to src/routes/(admin)/+layout.svelte

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script>
2-
import '../../../app.css'
2+
import '../../app.css'
33
import { Menu, Bell, User, Search, FileText, Settings, ChartBar, Home, X, LogOut } from '@lucide/svelte';
44
5-
/** @type {{ data: import('./$types').LayoutData, children: import('svelte').Snippet }} */
5+
/** @type {{ data: import('./admin/$types').LayoutData, children: import('svelte').Snippet }} */
66
let { data, children } = $props();
77
88
let mobileMenuOpen = $state(false);
@@ -31,6 +31,10 @@
3131
<FileText class="w-4 h-4 mr-2" />
3232
Blog Posts
3333
</a>
34+
<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">
35+
<User class="w-4 h-4 mr-2" />
36+
Contact Submissions
37+
</a>
3438
<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">
3539
<ChartBar class="w-4 h-4 mr-2" />
3640
Newsletter
@@ -76,6 +80,10 @@
7680
<FileText class="w-5 h-5 mr-3" />
7781
Blog Posts
7882
</a>
83+
<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}>
84+
<User class="w-5 h-5 mr-3" />
85+
Contact Submissions
86+
</a>
7987
<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}>
8088
<ChartBar class="w-5 h-5 mr-3" />
8189
Analytics
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import prisma from '$lib/prisma';
2+
/** @type {import('./$types').PageServerLoad} */
3+
export async function load() {
4+
const contacts = await prisma.contactSubmission.findMany();
5+
return { contacts };
6+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<script>
2+
/** @type {{ data: import('./$types').PageData }} */
3+
let { data } = $props();
4+
5+
const formatDate = (dateString) => {
6+
return new Date(dateString).toLocaleDateString('en-US', {
7+
year: 'numeric',
8+
month: 'short',
9+
day: 'numeric',
10+
hour: '2-digit',
11+
minute: '2-digit'
12+
});
13+
};
14+
</script>
15+
16+
<div class="p-6">
17+
<div class="mb-6">
18+
<h1 class="text-3xl font-bold text-gray-900">Contact Submissions</h1>
19+
</div>
20+
21+
{#if data.contacts && data.contacts.length > 0}
22+
<div class="overflow-x-auto bg-white shadow-lg rounded-lg">
23+
<table class="min-w-full divide-y divide-gray-200">
24+
<thead class="bg-gray-50">
25+
<tr>
26+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
27+
Contact Info
28+
</th>
29+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
30+
Reason
31+
</th>
32+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
33+
Message
34+
</th>
35+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
36+
Submitted
37+
</th>
38+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
39+
Tracking
40+
</th>
41+
</tr>
42+
</thead>
43+
<tbody class="bg-white divide-y divide-gray-200">
44+
{#each data.contacts as contact}
45+
<tr class="hover:bg-gray-50 transition-colors">
46+
<td class="px-6 py-4 whitespace-nowrap">
47+
<div class="flex flex-col">
48+
<div class="text-sm font-medium text-gray-900">{contact.name}</div>
49+
<div class="text-sm text-gray-500">{contact.email}</div>
50+
</div>
51+
</td>
52+
<td class="px-6 py-4 whitespace-nowrap">
53+
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-blue-100 text-blue-800">
54+
{contact.reason}
55+
</span>
56+
</td>
57+
<td class="px-6 py-4">
58+
<div class="text-sm text-gray-900 max-w-xs truncate" title={contact.message}>
59+
{contact.message}
60+
</div>
61+
</td>
62+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
63+
{formatDate(contact.createdAt)}
64+
</td>
65+
<td class="px-6 py-4">
66+
<div class="text-xs text-gray-500 space-y-1">
67+
{#if contact.ipAddress}
68+
<div>IP: {contact.ipAddress}</div>
69+
{/if}
70+
{#if contact.referrer}
71+
<div class="truncate max-w-32" title={contact.referrer}>
72+
Ref: {contact.referrer}
73+
</div>
74+
{/if}
75+
</div>
76+
</td>
77+
</tr>
78+
{/each}
79+
</tbody>
80+
</table>
81+
</div>
82+
83+
<div class="mt-4 text-sm text-gray-600">
84+
Total submissions: {data.contacts.length}
85+
</div>
86+
{:else}
87+
<div class="text-center py-12">
88+
<div class="mx-auto h-12 w-12 text-gray-400">
89+
<svg fill="none" viewBox="0 0 24 24" stroke="currentColor">
90+
<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"/>
91+
</svg>
92+
</div>
93+
<h3 class="mt-2 text-sm font-medium text-gray-900">No contact submissions</h3>
94+
<p class="mt-1 text-sm text-gray-500">No contact form requests have been submitted yet.</p>
95+
</div>
96+
{/if}
97+
</div>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,110 @@
1+
import prisma from '$lib/prisma';
2+
import { fail } from '@sveltejs/kit';
3+
14
/** @type {import('./$types').PageServerLoad} */
25
export async function load() {
36
return {};
7+
}
8+
9+
/** @type {import('./$types').Actions} */
10+
export const actions = {
11+
default: async ({ request }) => {
12+
13+
14+
const data = await request.formData();
15+
const name = data.get('name');
16+
const email = data.get('email');
17+
const serviceType = data.get('serviceType');
18+
const message = data.get('message');
19+
20+
// Server-side validation
21+
const errors = {};
22+
23+
if (!name || name.toString().trim() === '') {
24+
errors.name = 'Name is required';
25+
}
26+
27+
if (!email || email.toString().trim() === '') {
28+
errors.email = 'Email is required';
29+
} else if (!/\S+@\S+\.\S+/.test(email.toString())) {
30+
errors.email = 'Email is invalid';
31+
}
32+
33+
if (!serviceType || serviceType.toString().trim() === '') {
34+
errors.serviceType = 'Please select a service type';
35+
}
36+
37+
if (!message || message.toString().trim() === '') {
38+
errors.message = 'Message is required';
39+
}
40+
41+
if (Object.keys(errors).length > 0) {
42+
return fail(400, {
43+
errors,
44+
name: name?.toString() || '',
45+
email: email?.toString() || '',
46+
serviceType: serviceType?.toString() || '',
47+
message: message?.toString() || ''
48+
});
49+
}
50+
51+
try {
52+
// Get client information from headers
53+
const userAgent = request.headers.get('user-agent');
54+
const forwarded = request.headers.get('x-forwarded-for');
55+
const realIp = request.headers.get('x-real-ip');
56+
const cfConnectingIp = request.headers.get('cf-connecting-ip');
57+
const referrer = request.headers.get('referer');
58+
59+
// Determine IP address (priority: CF > X-Real-IP > X-Forwarded-For)
60+
let ipAddress = cfConnectingIp || realIp;
61+
if (!ipAddress && forwarded) {
62+
ipAddress = forwarded.split(',')[0].trim();
63+
}
64+
65+
66+
// Store submission in database
67+
const submission = await prisma.contactSubmission.create({
68+
data: {
69+
name: name.toString().trim(),
70+
email: email.toString().trim(),
71+
reason: serviceType.toString().trim(),
72+
message: message.toString().trim(),
73+
ipAddress,
74+
userAgent,
75+
referrer
76+
}
77+
});
78+
79+
80+
return {
81+
success: true,
82+
message: 'Thank you for your message! We\'ll get back to you within 24 hours.'
83+
};
84+
85+
} catch (error) {
86+
console.error('Error saving contact submission:', error);
87+
88+
// More specific error handling
89+
if (error.code === 'P1001') {
90+
return fail(500, {
91+
error: 'Database connection failed. Please try again later.',
92+
name: name?.toString() || '',
93+
email: email?.toString() || '',
94+
serviceType: serviceType?.toString() || '',
95+
message: message?.toString() || ''
96+
});
97+
}
98+
99+
return fail(500, {
100+
error: 'Sorry, there was an error submitting your message. Please try again later.',
101+
name: name?.toString() || '',
102+
email: email?.toString() || '',
103+
serviceType: serviceType?.toString() || '',
104+
message: message?.toString() || ''
105+
});
106+
} finally {
107+
108+
}
109+
}
4110
};

0 commit comments

Comments
 (0)