Compare commits
18
Commits
d525fea931
...
5a68c112cb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a68c112cb | ||
|
|
5898e97a27 | ||
|
|
fd024874c7 | ||
|
|
683ff54cdb | ||
|
|
da0e2367c0 | ||
|
|
40ed89793b | ||
|
|
48058c544e | ||
|
|
0213226882 | ||
|
|
4d882f1dc7 | ||
|
|
68e1623c1b | ||
|
|
d002516bc8 | ||
|
|
3c46684b1b | ||
|
|
8f6e530dc4 | ||
|
|
9fcf2e73fe | ||
|
|
c7bc4c724f | ||
|
|
91c9b2e69b | ||
|
|
45cc2814b9 | ||
|
|
c2677881a7 |
@@ -1,7 +1,7 @@
|
||||
# Graph Report - /home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f (2026-04-12)
|
||||
# Graph Report - /home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f (2026-04-13)
|
||||
|
||||
## Corpus Check
|
||||
- 43 files · ~210,827 words
|
||||
- 43 files · ~211,373 words
|
||||
- Verdict: corpus is large enough that graph structure adds value.
|
||||
|
||||
## Summary
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
+16
-16
@@ -103,7 +103,7 @@
|
||||
"label": "sendContactEmail()",
|
||||
"file_type": "code",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L96",
|
||||
"source_location": "L150",
|
||||
"id": "contact_sendcontactemail",
|
||||
"community": 5
|
||||
},
|
||||
@@ -111,7 +111,7 @@
|
||||
"label": "escapeHtml()",
|
||||
"file_type": "code",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L181",
|
||||
"source_location": "L288",
|
||||
"id": "contact_escapehtml",
|
||||
"community": 5
|
||||
},
|
||||
@@ -119,7 +119,7 @@
|
||||
"label": "corsHeaders()",
|
||||
"file_type": "code",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L195",
|
||||
"source_location": "L302",
|
||||
"id": "contact_corsheaders",
|
||||
"community": 5
|
||||
},
|
||||
@@ -127,7 +127,7 @@
|
||||
"label": "OPTIONS()",
|
||||
"file_type": "code",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L220",
|
||||
"source_location": "L327",
|
||||
"id": "contact_options",
|
||||
"community": 5
|
||||
},
|
||||
@@ -135,7 +135,7 @@
|
||||
"label": "POST()",
|
||||
"file_type": "code",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L227",
|
||||
"source_location": "L334",
|
||||
"id": "contact_post",
|
||||
"community": 5
|
||||
},
|
||||
@@ -1421,7 +1421,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L96",
|
||||
"source_location": "L150",
|
||||
"weight": 1.0,
|
||||
"_src": "contact",
|
||||
"_tgt": "contact_sendcontactemail",
|
||||
@@ -1433,7 +1433,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L181",
|
||||
"source_location": "L288",
|
||||
"weight": 1.0,
|
||||
"_src": "contact",
|
||||
"_tgt": "contact_escapehtml",
|
||||
@@ -1445,7 +1445,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L195",
|
||||
"source_location": "L302",
|
||||
"weight": 1.0,
|
||||
"_src": "contact",
|
||||
"_tgt": "contact_corsheaders",
|
||||
@@ -1457,7 +1457,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L220",
|
||||
"source_location": "L327",
|
||||
"weight": 1.0,
|
||||
"_src": "contact",
|
||||
"_tgt": "contact_options",
|
||||
@@ -1469,7 +1469,7 @@
|
||||
"relation": "contains",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L227",
|
||||
"source_location": "L334",
|
||||
"weight": 1.0,
|
||||
"_src": "contact",
|
||||
"_tgt": "contact_post",
|
||||
@@ -1481,7 +1481,7 @@
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L233",
|
||||
"source_location": "L340",
|
||||
"weight": 1.0,
|
||||
"_src": "contact_post",
|
||||
"_tgt": "contact_checkratelimit",
|
||||
@@ -1493,7 +1493,7 @@
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L297",
|
||||
"source_location": "L404",
|
||||
"weight": 1.0,
|
||||
"_src": "contact_post",
|
||||
"_tgt": "contact_validatecontactform",
|
||||
@@ -1505,7 +1505,7 @@
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L155",
|
||||
"source_location": "L221",
|
||||
"weight": 1.0,
|
||||
"_src": "contact_sendcontactemail",
|
||||
"_tgt": "contact_escapehtml",
|
||||
@@ -1517,7 +1517,7 @@
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L307",
|
||||
"source_location": "L417",
|
||||
"weight": 1.0,
|
||||
"_src": "contact_post",
|
||||
"_tgt": "contact_sendcontactemail",
|
||||
@@ -1529,7 +1529,7 @@
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L221",
|
||||
"source_location": "L328",
|
||||
"weight": 1.0,
|
||||
"_src": "contact_options",
|
||||
"_tgt": "contact_corsheaders",
|
||||
@@ -1541,7 +1541,7 @@
|
||||
"relation": "calls",
|
||||
"confidence": "EXTRACTED",
|
||||
"source_file": "/home/agent/ai-agent/backend/routes/../workspace/73f46153-e8cc-447a-82ad-2eeefa00689f/src/pages/api/contact.ts",
|
||||
"source_location": "L235",
|
||||
"source_location": "L342",
|
||||
"weight": 1.0,
|
||||
"_src": "contact_post",
|
||||
"_tgt": "contact_corsheaders",
|
||||
|
||||
Generated
+13
@@ -13,6 +13,7 @@
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"astro": "^4.5.0",
|
||||
"compression": "^1.8.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"nodemailer": "^8.0.3"
|
||||
},
|
||||
@@ -2812,6 +2813,18 @@
|
||||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.4.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
|
||||
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dset": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"astro": "^4.5.0",
|
||||
"compression": "^1.8.1",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"nodemailer": "^8.0.3"
|
||||
},
|
||||
|
||||
+11
-3
@@ -1,12 +1,20 @@
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
import { config } from 'dotenv';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { dirname, join } from 'path';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
// Load .env from the project root (same dir as server.mjs)
|
||||
config({ path: join(__dirname, '.env') });
|
||||
console.log('[server] Loaded .env from:', join(__dirname, '.env'));
|
||||
console.log('[server] SMTP_HOST:', process.env.SMTP_HOST || 'NOT SET');
|
||||
console.log('[server] CONTACT_EMAIL_TO:', process.env.CONTACT_EMAIL_TO || 'NOT SET');
|
||||
|
||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||
import express from 'express';
|
||||
import compression from 'compression';
|
||||
|
||||
const app = express();
|
||||
|
||||
// Server configuration
|
||||
|
||||
+171
-34
@@ -90,15 +90,70 @@ function validateContactForm(data: ContactFormData): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// SMTP transporter (created once at module level for reuse)
|
||||
// ============================================================
|
||||
let smtpTransporter: any = null;
|
||||
let smtpVerified = false;
|
||||
|
||||
// Verify SMTP connection once at module initialization
|
||||
(async () => {
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
const smtpPort = parseInt(process.env.SMTP_PORT ?? '587', 10);
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass) {
|
||||
logger.info('api.contact', 'SMTP not configured — skipping connection verification');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const nodemailer = await import('nodemailer');
|
||||
smtpTransporter = nodemailer.default.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: false, // Port 587 uses STARTTLS, not SSL
|
||||
auth: { user: smtpUser, pass: smtpPass },
|
||||
requireTLS: true, // Force STARTTLS upgrade
|
||||
tls: {
|
||||
rejectUnauthorized: false, // Handle Hostinger certificate issues
|
||||
},
|
||||
});
|
||||
|
||||
await smtpTransporter.verify();
|
||||
smtpVerified = true;
|
||||
console.log('SMTP connection verified successfully');
|
||||
logger.info('api.contact', 'SMTP connection verified successfully', {
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
user: smtpUser,
|
||||
});
|
||||
} catch (err: any) {
|
||||
smtpVerified = false;
|
||||
console.error('SMTP connection verification failed:', {
|
||||
code: err?.code,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
});
|
||||
logger.error('api.contact', 'SMTP connection verification failed', {
|
||||
code: err?.code,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
// ============================================================
|
||||
// Email sending via SMTP (nodemailer) or no-op in development
|
||||
// ============================================================
|
||||
async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
const smtpHost = import.meta.env.SMTP_HOST;
|
||||
const smtpUser = import.meta.env.SMTP_USER;
|
||||
const smtpPass = import.meta.env.SMTP_PASS;
|
||||
const smtpPort = parseInt(import.meta.env.SMTP_PORT ?? '587', 10);
|
||||
const recipientEmail = import.meta.env.CONTACT_EMAIL ?? import.meta.env.SMTP_USER;
|
||||
const smtpHost = process.env.SMTP_HOST;
|
||||
const smtpUser = process.env.SMTP_USER;
|
||||
const smtpPass = process.env.SMTP_PASS;
|
||||
const smtpPort = parseInt(process.env.SMTP_PORT ?? '587', 10);
|
||||
const smtpFrom = process.env.SMTP_FROM;
|
||||
const recipientEmail = process.env.CONTACT_EMAIL_TO;
|
||||
|
||||
if (!smtpHost || !smtpUser || !smtpPass || !recipientEmail) {
|
||||
// SMTP not configured - log and continue (graceful degradation)
|
||||
@@ -112,13 +167,20 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
|
||||
// Dynamic import to avoid bundling issues when nodemailer isn't installed
|
||||
try {
|
||||
const nodemailer = await import('nodemailer');
|
||||
const transporter = nodemailer.default.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: smtpPort === 465,
|
||||
auth: { user: smtpUser, pass: smtpPass },
|
||||
});
|
||||
// Reuse module-level transporter if available, otherwise create a new one
|
||||
if (!smtpTransporter) {
|
||||
const nodemailer = await import('nodemailer');
|
||||
smtpTransporter = nodemailer.default.createTransport({
|
||||
host: smtpHost,
|
||||
port: smtpPort,
|
||||
secure: false,
|
||||
auth: { user: smtpUser, pass: smtpPass },
|
||||
requireTLS: true,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const subjectLabels: Record<string, string> = {
|
||||
'web-development': 'Web Development',
|
||||
@@ -130,9 +192,13 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
'other': 'Other',
|
||||
};
|
||||
|
||||
await transporter.sendMail({
|
||||
from: `"WorkRoot Contact Form" <${smtpUser}>`,
|
||||
const fromAddress = smtpFrom || smtpUser;
|
||||
|
||||
// 1) Admin notification email with BCC to SMTP_USER for delivery confirmation
|
||||
const adminInfo = await smtpTransporter.sendMail({
|
||||
from: `"CompanySite Contact Form" <${fromAddress}>`,
|
||||
to: recipientEmail,
|
||||
bcc: process.env.SMTP_USER, // BCC to admin inbox as delivery confirmation
|
||||
replyTo: data.email,
|
||||
subject: `[Contact Form] ${subjectLabels[data.subject] ?? data.subject} - ${data.name}`,
|
||||
text: [
|
||||
@@ -161,6 +227,47 @@ async function sendContactEmail(data: ContactFormData): Promise<void> {
|
||||
<p style="color:#888;font-size:12px;margin-top:16px">Submitted at: ${new Date().toISOString()}</p>
|
||||
`,
|
||||
});
|
||||
|
||||
// 2) User confirmation email — proves delivery works AND provides good UX
|
||||
const userConfirmInfo = await smtpTransporter.sendMail({
|
||||
from: `"CompanySite" <${fromAddress}>`,
|
||||
to: data.email,
|
||||
subject: 'We received your message - CompanySite',
|
||||
text: [
|
||||
`Hi ${data.name},`,
|
||||
``,
|
||||
`Thank you for reaching out to us! We have received your message regarding "${subjectLabels[data.subject] ?? data.subject}".`,
|
||||
``,
|
||||
`Our team will review your inquiry and get back to you as soon as possible, typically within 1-2 business days.`,
|
||||
``,
|
||||
`For your reference, here is a summary of your submission:`,
|
||||
` Subject: ${subjectLabels[data.subject] ?? data.subject}`,
|
||||
` Date: ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`,
|
||||
``,
|
||||
`If you have any urgent questions, feel free to reply to this email.`,
|
||||
``,
|
||||
`Best regards,`,
|
||||
`The CompanySite Team`,
|
||||
].join('\n'),
|
||||
html: `
|
||||
<div style="font-family:Arial,sans-serif;max-width:600px;margin:0 auto;padding:20px">
|
||||
<h2 style="color:#333">Thank you for contacting us!</h2>
|
||||
<p>Hi ${escapeHtml(data.name)},</p>
|
||||
<p>We have received your message regarding <strong>"${escapeHtml(subjectLabels[data.subject] ?? data.subject)}"</strong>.</p>
|
||||
<p>Our team will review your inquiry and get back to you as soon as possible, typically within <strong>1-2 business days</strong>.</p>
|
||||
<div style="background:#f9f9f9;border-left:4px solid #007bff;padding:12px 16px;margin:20px 0">
|
||||
<p style="margin:4px 0"><strong>Subject:</strong> ${escapeHtml(subjectLabels[data.subject] ?? data.subject)}</p>
|
||||
<p style="margin:4px 0"><strong>Date:</strong> ${new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
||||
</div>
|
||||
<p>If you have any urgent questions, feel free to reply to this email.</p>
|
||||
<p style="margin-top:24px">Best regards,<br><strong>The CompanySite Team</strong></p>
|
||||
<hr style="border:none;border-top:1px solid #eee;margin-top:24px">
|
||||
<p style="color:#888;font-size:12px">This is an automated confirmation. Please do not reply unless you need further assistance.</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
return { adminInfo, userConfirmInfo };
|
||||
} catch (err) {
|
||||
// If nodemailer isn't installed, treat as graceful degradation (log only)
|
||||
if (err instanceof Error && err.message.includes('Cannot find module')) {
|
||||
@@ -302,26 +409,56 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
);
|
||||
}
|
||||
|
||||
// Send email
|
||||
try {
|
||||
await sendContactEmail(formData);
|
||||
} catch {
|
||||
logApiRequest({
|
||||
scope: 'api.contact',
|
||||
method: 'POST',
|
||||
path: '/api/contact',
|
||||
status: 500,
|
||||
ip,
|
||||
durationMs: Date.now() - startTime,
|
||||
// Fire-and-forget: send email in background so the user gets an instant response.
|
||||
// Errors are logged/reported but never block the HTTP reply.
|
||||
const recipientEmail = process.env.CONTACT_EMAIL_TO || 'unknown';
|
||||
console.log('Sending contact email to: ' + recipientEmail);
|
||||
|
||||
sendContactEmail(formData)
|
||||
.then((result) => {
|
||||
console.log('Contact emails delivered successfully:', {
|
||||
adminRecipient: recipientEmail,
|
||||
adminMessageId: result?.adminInfo?.messageId,
|
||||
userRecipient: formData.email,
|
||||
userMessageId: result?.userConfirmInfo?.messageId,
|
||||
});
|
||||
logger.info('api.contact', 'Contact emails delivered successfully', {
|
||||
adminRecipient: recipientEmail,
|
||||
adminMessageId: result?.adminInfo?.messageId,
|
||||
userRecipient: formData.email,
|
||||
userMessageId: result?.userConfirmInfo?.messageId,
|
||||
subject: formData.subject,
|
||||
});
|
||||
})
|
||||
.catch(async (err: any) => {
|
||||
console.error('Background email delivery failed:', {
|
||||
code: err?.code,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
responseCode: err?.responseCode,
|
||||
recipient: recipientEmail,
|
||||
email: formData.email,
|
||||
subject: formData.subject,
|
||||
});
|
||||
logger.error('api.contact', 'Background email delivery failed', {
|
||||
code: err?.code,
|
||||
message: err?.message,
|
||||
response: err?.response,
|
||||
responseCode: err?.responseCode,
|
||||
email: formData.email,
|
||||
subject: formData.subject,
|
||||
});
|
||||
await captureException(err, {
|
||||
scope: 'api.contact.background',
|
||||
extra: {
|
||||
email: formData.email,
|
||||
subject: formData.subject,
|
||||
smtpCode: err?.code,
|
||||
smtpResponse: err?.response,
|
||||
smtpResponseCode: err?.responseCode,
|
||||
},
|
||||
});
|
||||
});
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: 'Failed to send message. Please try again or email us directly.',
|
||||
}),
|
||||
{ status: 500, headers: rateLimitHeaders }
|
||||
);
|
||||
}
|
||||
|
||||
logApiRequest({
|
||||
scope: 'api.contact',
|
||||
@@ -334,7 +471,7 @@ export const POST: APIRoute = async ({ request, clientAddress }) => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
message: "Message sent successfully! We'll get back to you within 24 hours.",
|
||||
message: 'Your message has been received. We will get back to you soon.',
|
||||
}),
|
||||
{ status: 200, headers: rateLimitHeaders }
|
||||
);
|
||||
|
||||
+24
-4
@@ -437,7 +437,7 @@ const trustStats = [
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-secondary-900 dark:text-secondary-100 mb-2">Message Sent!</h3>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6">Thank you for reaching out. We'll get back to you within 24 hours.</p>
|
||||
<p class="text-secondary-600 dark:text-secondary-400 mb-6">Message received! We will get back to you shortly.</p>
|
||||
<button
|
||||
id="send-another"
|
||||
class="inline-flex items-center gap-2 text-primary hover:text-primary-700 font-medium text-sm transition-colors"
|
||||
@@ -1022,10 +1022,18 @@ const trustStats = [
|
||||
let success = false;
|
||||
let errorMessage = 'Something went wrong. Please try again.';
|
||||
|
||||
// Track when loading started for minimum display time
|
||||
const loadingStart = Date.now();
|
||||
|
||||
try {
|
||||
// Set up a 10-second timeout using AbortController
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10_000);
|
||||
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
signal: controller.signal,
|
||||
body: JSON.stringify({
|
||||
name: (form.elements.namedItem('name') as HTMLInputElement)?.value.trim(),
|
||||
email: (form.elements.namedItem('email') as HTMLInputElement)?.value.trim(),
|
||||
@@ -1036,6 +1044,8 @@ const trustStats = [
|
||||
}),
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
const data = await response.json() as { success: boolean; message?: string; error?: string };
|
||||
|
||||
if (response.ok && data.success) {
|
||||
@@ -1043,8 +1053,18 @@ const trustStats = [
|
||||
} else {
|
||||
errorMessage = data.error ?? 'Failed to send message. Please try again.';
|
||||
}
|
||||
} catch {
|
||||
errorMessage = 'Network error. Please check your connection and try again.';
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||
errorMessage = 'Request timed out. Please check your connection and try again.';
|
||||
} else {
|
||||
errorMessage = 'Network error. Please check your connection and try again.';
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure loading state is shown for at least 300ms to avoid jarring flash
|
||||
const elapsed = Date.now() - loadingStart;
|
||||
if (elapsed < 300) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 300 - elapsed));
|
||||
}
|
||||
|
||||
// Track analytics
|
||||
@@ -1062,7 +1082,7 @@ const trustStats = [
|
||||
// Show success state inline
|
||||
form.classList.add('hidden');
|
||||
if (successState) successState.classList.remove('hidden');
|
||||
showToast("Message sent! We'll get back to you within 24 hours.", 'success');
|
||||
showToast('Message received! We will get back to you shortly.', 'success');
|
||||
} else {
|
||||
showToast(errorMessage, 'error');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user