API based email sender

This commit is contained in:
2026-07-14 12:14:23 +02:00
parent 7644e4768c
commit bcb84f4de4
11 changed files with 295 additions and 193 deletions
+9
View File
@@ -0,0 +1,9 @@
# GEMINI_API_KEY: Required for Gemini AI API calls.
# AI Studio automatically injects this at runtime from user secrets.
# Users configure this via the Secrets panel in the AI Studio UI.
GEMINI_API_KEY="MY_GEMINI_API_KEY"
# APP_URL: The URL where this applet is hosted.
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"
+3 -1
View File
@@ -3,7 +3,9 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>LiteCharms IT Services</title>
<title>Lite Charms</title>
<!-- SMTPJS direct mail client -->
<script src="https://smtpjs.com/v3/smtp.js" async></script>
</head>
<body>
<div id="root"></div>
-2
View File
@@ -18,7 +18,6 @@
"express": "^4.21.2",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"nodemailer": "^9.0.3",
"react": "^19.0.1",
"react-dom": "^19.0.1",
"vite": "^6.2.3"
@@ -26,7 +25,6 @@
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"@types/nodemailer": "^8.0.1",
"autoprefixer": "^10.4.21",
"esbuild": "^0.25.0",
"tailwindcss": "^4.1.14",
+73 -120
View File
@@ -1,7 +1,6 @@
import express from 'express';
import path from 'path';
import fs from 'fs';
import nodemailer from 'nodemailer';
import { createServer as createViteServer } from 'vite';
import dotenv from 'dotenv';
@@ -13,156 +12,110 @@ async function startServer() {
app.use(express.json());
// API route to send email via SMTP
app.post('/api/send-email', async (req, res) => {
// API route to send email via SMTP (supporting both standard and PHP endpoint pathways)
app.post(['/api/send-email', '/send-email.php'], async (req, res) => {
const { name, email, phone, company, subject, message, isEstimate, estimateSummary } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Missing name or email' });
}
// Load SMTP configuration (favoring env variables, falling back to JSON file)
const smtpConfig = {
host: process.env.SMTP_HOST || 'smtp.khongisa.co.za',
port: Number(process.env.SMTP_PORT || 587),
secure: process.env.SMTP_SECURE === 'true',
user: process.env.SMTP_USER || '',
pass: process.env.SMTP_PASS || '',
fromEmail: process.env.SMTP_FROM || '',
toEmail: process.env.SMTP_TO || 'info@litecharms.co.za',
};
// If environment variables aren't completely defined, try loading from smtp-config.json
const configPath = path.join(process.cwd(), 'smtp-config.json');
if ((!smtpConfig.user || !smtpConfig.pass || smtpConfig.pass.includes('YOUR_SECURE')) && fs.existsSync(configPath)) {
try {
const fileData = fs.readFileSync(configPath, 'utf8');
const parsed = JSON.parse(fileData);
smtpConfig.host = parsed.host || smtpConfig.host;
smtpConfig.port = Number(parsed.port || smtpConfig.port);
smtpConfig.secure = parsed.secure ?? smtpConfig.secure;
smtpConfig.user = parsed.auth?.user || smtpConfig.user;
smtpConfig.pass = parsed.auth?.pass || smtpConfig.pass;
smtpConfig.fromEmail = parsed.fromEmail || smtpConfig.fromEmail;
smtpConfig.toEmail = parsed.toEmail || smtpConfig.toEmail;
} catch (err) {
console.error('Error reading smtp-config.json:', err);
}
}
// Generate Email Content
let emailSubject = subject || 'LiteCharms Inquiry';
if (isEstimate) {
emailSubject = `LiteCharms Project Estimate Proposal: ${subject}`;
const emailSubject = isEstimate
? `LiteCharms Project Estimate Proposal: ${subject}`
: `LiteCharms General Service Inquiry: ${subject}`;
let breakdownHtml = '';
if (isEstimate && estimateSummary && Array.isArray(estimateSummary.breakdown)) {
breakdownHtml = estimateSummary.breakdown
.map((b: any) => `<li>${b.item}: ${b.price}</li>`)
.join('');
}
let emailHtml = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; color: #1e293b;">
<div style="background-color: #020617; padding: 24px; text-align: center; border-bottom: 2px solid #00bcac;">
<h2 style="color: #ffffff; margin: 0; font-size: 22px; letter-spacing: -0.02em;">Lite<span style="color: #00bcac;">Charms</span></h2>
<p style="color: #94a3b8; margin: 4px 0 0 0; font-size: 12px; font-family: monospace;">MIDRAND, SOUTH AFRICA</p>
</div>
<div style="padding: 24px; background-color: #ffffff;">
<h3 style="color: #00bcac; margin-top: 0; margin-bottom: 16px; font-size: 18px;">${isEstimate ? 'New Project Estimate Inquiry' : 'New General Service Inquiry'}</h3>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
const emailHtml = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; color: #1e293b;">
<div style="background-color: #020617; padding: 24px; text-align: center; border-bottom: 2px solid #00bcac;">
<h2 style="color: #ffffff; margin: 0; font-size: 22px; letter-spacing: -0.02em;">Lite<span style="color: #00bcac;">Charms</span></h2>
<p style="color: #94a3b8; margin: 4px 0 0 0; font-size: 12px; font-family: monospace;">MIDRAND, SOUTH AFRICA</p>
</div>
<div style="padding: 24px; background-color: #ffffff;">
<h3 style="color: #00bcac; margin-top: 0; margin-bottom: 16px; font-size: 18px;">${isEstimate ? 'New Project Estimate Inquiry' : 'New General Service Inquiry'}</h3>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; width: 120px; font-size: 13px; color: #64748b;">Client Name:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${name}</td>
<td style="padding: 8px 0; font-weight: bold; width: 120px; font-size: 13px; color: #64748b;">Client Name:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${name}</td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Email Address:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;"><a href="mailto:${email}">${email}</a></td>
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Email Address:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;"><a href="mailto:${email}">${email}</a></td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Phone Number:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${phone || 'Not Provided'}</td>
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Phone Number:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${phone || 'Not Provided'}</td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Company:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${company || 'Not Provided'}</td>
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Company:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${company || 'Not Provided'}</td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Subject:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${subject || 'General Service Contact'}</td>
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Subject:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${subject}</td>
</tr>
</table>
`;
</table>
if (isEstimate && estimateSummary) {
emailHtml += `
${isEstimate && estimateSummary ? `
<div style="background-color: #f8fafc; border: 1px dashed #cbd5e1; padding: 16px; border-radius: 6px; margin-bottom: 24px;">
<h4 style="margin-top: 0; color: #020617; font-size: 14px; border-bottom: 1px solid #e2e8f0; padding-bottom: 6px;">Selected Scope & Custom Pricing Range</h4>
<div style="margin-top: 8px; font-size: 13px;">
<strong>Estimated Range:</strong> <span style="color: #00bcac; font-weight: bold;">R ${estimateSummary.totalMin?.toLocaleString('en-ZA')}</span> to <span style="color: #e11d48; font-weight: bold;">R ${estimateSummary.totalMax?.toLocaleString('en-ZA')}</span>
<br/><strong>Roadmap:</strong> ~${estimateSummary.timelineWeeks} Weeks
</div>
<div style="margin-top: 12px; font-size: 11px; color: #64748b;">
<strong>Breakdown items:</strong>
<ul style="margin: 4px 0 0 0; padding-left: 16px;">
${(estimateSummary.breakdown || []).map((b: any) => `<li>${b.item}: ${b.price}</li>`).join('')}
</ul>
</div>
<h4 style="margin-top: 0; color: #020617; font-size: 14px; border-bottom: 1px solid #e2e8f0; padding-bottom: 6px;">Selected Scope & Custom Pricing Range</h4>
<div style="margin-top: 8px; font-size: 13px;">
<strong>Estimated Range:</strong> <span style="color: #00bcac; font-weight: bold;">R ${estimateSummary.totalMin?.toLocaleString('en-ZA')}</span> to <span style="color: #e11d48; font-weight: bold;">R ${estimateSummary.totalMax?.toLocaleString('en-ZA')}</span>
<br/><strong>Roadmap:</strong> ~${estimateSummary.timelineWeeks} Weeks
</div>
${breakdownHtml ? `
<div style="margin-top: 12px; font-size: 11px; color: #64748b;">
<strong>Breakdown items:</strong>
<ul style="margin: 4px 0 0 0; padding-left: 16px;">
${breakdownHtml}
</ul>
</div>
` : ''}
</div>
`;
}
` : ''}
emailHtml += `
<div style="margin-bottom: 24px;">
<div style="margin-bottom: 24px;">
<h4 style="margin-top: 0; margin-bottom: 8px; font-size: 14px; color: #020617;">Client Message / Brief:</h4>
<p style="background-color: #f8fafc; padding: 12px; border-radius: 6px; font-size: 13px; line-height: 1.5; color: #334155; white-space: pre-wrap; margin: 0;">${message || 'No additional notes provided.'}</p>
</div>
<p style="background-color: #f8fafc; padding: 12px; border-radius: 6px; font-size: 13px; line-height: 1.5; color: #334155; white-space: pre-wrap; margin: 0;">${(message || 'No additional notes provided.').replace(/\n/g, '<br/>')}</p>
</div>
<div style="background-color: #f1f5f9; padding: 16px; text-align: center; font-size: 11px; color: #64748b; border-top: 1px solid #e2e8f0;">
This is an automated dispatch from khongisa.co.za Private Cloud platform for LiteCharms (PTY) Ltd.
</div>
</div>
`;
// Check if SMTP is ready or left as placeholder
const hasValidAuth = smtpConfig.user && smtpConfig.pass && !smtpConfig.pass.includes('YOUR_SECURE');
if (!hasValidAuth) {
console.log('\n--- SMTP EMAIL LOG (NO PASSWORD CONFIGURED) ---');
console.log(`To: ${smtpConfig.toEmail}`);
console.log(`From: ${smtpConfig.fromEmail || 'no-reply@khongisa.co.za'}`);
console.log(`Subject: ${emailSubject}`);
console.log(`Plain text summary:\nClient ${name} (${email}) sent inquiry. Subject: ${emailSubject}`);
console.log('-----------------------------------------------\n');
return res.status(200).json({
success: true,
message: 'SMTP credentials not configured yet. Email dispatch was logged in system console successfully.',
logged: true
});
}
</div>
<div style="background-color: #f1f5f9; padding: 16px; text-align: center; font-size: 11px; color: #64748b; border-top: 1px solid #e2e8f0;">
This is an automated dispatch from the LiteCharms website server.
</div>
</div>
`;
try {
const transporter = nodemailer.createTransport({
host: smtpConfig.host,
port: smtpConfig.port,
secure: smtpConfig.secure,
auth: {
user: smtpConfig.user,
pass: smtpConfig.pass,
const response = await fetch('https://messenger.api.khongisa.co.za/api/send-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
htmlBody: emailHtml,
from: 'contact@litecharms.co.za',
to: 'contact@litecharms.co.za',
subject: emailSubject,
}),
});
await transporter.sendMail({
from: smtpConfig.fromEmail || smtpConfig.user,
to: smtpConfig.toEmail,
subject: emailSubject,
html: emailHtml,
});
return res.json({ success: true, message: 'Email sent successfully via SMTP' });
if (response.ok) {
return res.json({ success: true, message: 'Email sent successfully via Messenger API' });
} else {
const errorText = await response.text();
return res.status(500).json({ error: 'External API Error', details: errorText });
}
} catch (error: any) {
console.error('SMTP Email Send Error:', error);
return res.status(500).json({
error: 'SMTP Connection failed',
details: error.message,
message: 'Failed to send email via SMTP post-deployment. Please verify your host, user, and password inside smtp-config.json.'
});
console.error('Server proxy email send error:', error);
return res.status(500).json({ error: 'Server error sending email', details: error.message });
}
});
-11
View File
@@ -1,11 +0,0 @@
{
"host": "mail.litecharms.co.za",
"port": 465,
"secure": true,
"auth": {
"user": "contact@litecharms.co.za",
"pass": "PPUzCl%TL$GD*x7f"
},
"fromEmail": "LiteCharms <contact@khongisa.co.za>",
"toEmail": "contact@litecharms.co.za"
}
+17 -20
View File
@@ -7,6 +7,7 @@ import FAQSection from './components/FAQSection';
import AuroraBackground from './components/AuroraBackground';
import { BrandIcon, BrandLogo } from './components/BrandAssets';
import { Inquiry } from './types';
import { sendEmailClientSide } from './utils/emailController';
import {
MapPin,
Mail,
@@ -28,8 +29,8 @@ export default function App() {
const [contactLoading, setContactLoading] = useState(false);
// Stats Counters state for load-in effect
const [activeClients, setActiveClients] = useState(12);
const [completedProjects, setCompletedProjects] = useState(25);
const [activeClients, setActiveClients] = useState(1);
const [completedProjects, setCompletedProjects] = useState(5);
useEffect(() => {
// Soft animate stats for visual delight
@@ -39,9 +40,9 @@ export default function App() {
clearInterval(clientsTimer);
return 4;
}
return prev + 2;
return prev + 1;
});
}, 40);
}, 150);
const projectsTimer = setInterval(() => {
setCompletedProjects((prev) => {
@@ -49,9 +50,9 @@ export default function App() {
clearInterval(projectsTimer);
return 25;
}
return prev + 4;
return prev + 1;
});
}, 25);
}, 40);
return () => {
clearInterval(clientsTimer);
@@ -93,19 +94,15 @@ export default function App() {
localStorage.setItem('litecharms_general_contacts', JSON.stringify(pastMsgs));
try {
await fetch('/api/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: contactName,
email: contactEmail,
subject: contactSubject,
message: contactMessage,
isEstimate: false,
}),
await sendEmailClientSide({
name: contactName,
email: contactEmail,
subject: contactSubject,
message: contactMessage,
isEstimate: false,
});
} catch (err) {
console.error('SMTP API Submission failed. Saved locally instead:', err);
console.error('SMTP direct send failed. Saved locally instead:', err);
} finally {
setContactLoading(false);
setContactSuccess(true);
@@ -267,10 +264,10 @@ export default function App() {
</h2>
<div className="space-y-4 text-sm leading-relaxed text-slate-300">
<p>
LiteCharms (PTY) Ltd was born in Midrand in 2020 , initially serving as a specialist interface consulting firm for the emerging mobile industry in South Africa. As local enterprise operations expanded, we observed a crucial bottleneck: software layout design cannot achieve its full potential if clients lack reliable, containerized hosting environments and flexible scale options.
LiteCharms (PTY) Ltd was born in Midrand in 2020, initially serving as a specialist interface consulting firm for the emerging mobile industry in South Africa. As local enterprise operations expanded, we observed a crucial bottleneck: software layout design cannot achieve its full potential if clients lack reliable, containerized hosting environments and flexible scale options.
</p>
<p>
In 2024, we expanded our core divisions to provide automated Docker container layouts, declarative Kubernetes blueprints, and cloud setups, powered by our high-performance private cloud platform, khongisa.co.za.
In 2022, we expanded our core divisions to provide automated Docker container layouts, declarative Kubernetes blueprints, and cloud setups, powered by our high-performance private cloud platform, khongisa.co.za.
</p>
<p className="font-medium text-white">
Today, we bridge the gap between user experience and modern cloud orchestration, delivering reliable structures that are optimized, secure, and fully documented.
@@ -281,7 +278,7 @@ export default function App() {
<div className="pt-6 border-t border-slate-800 space-y-3 text-xs">
<div className="flex items-center space-x-3 text-slate-300">
<MapPin className="w-4 h-4 text-brand-500 shrink-0" />
<span>1633 Liebenburg Rd, Noordwyk, Midrand, Gauteng, ZA</span>
<span>1633 Liebenburg Rd, Noordwyk, Midrand, 1687, South Africa</span>
</div>
<div className="flex items-center space-x-3 text-slate-300">
<Clock className="w-4 h-4 text-brand-500 shrink-0" />
+3 -3
View File
@@ -30,12 +30,12 @@ export default function FAQSection() {
className="w-full flex items-center justify-between p-6 text-left font-display font-semibold text-white hover:text-brand-400 transition-colors cursor-pointer"
>
<div className="flex items-center space-x-3.5 pr-4">
<HelpCircle className={`w-5 h-5 shrink-0 transition-colors ${isOpen ? 'text-brand-500' : 'text-slate-500'}`} />
<HelpCircle className={`w-5 h-5 shrink-0 transition-colors ${isOpen ? 'text-brand-400' : 'text-slate-400'}`} />
<span className="text-sm md:text-base leading-snug">{faq.question}</span>
</div>
<ChevronDown
className={`w-4 h-4 text-slate-500 shrink-0 transition-transform duration-300 ${
isOpen ? 'rotate-180 text-brand-500' : ''
className={`w-4 h-4 text-slate-400 shrink-0 transition-transform duration-300 ${
isOpen ? 'rotate-180 text-brand-400' : ''
}`}
/>
</button>
+34 -23
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, FormEvent } from 'react';
import { useState, useEffect, FormEvent, useRef } from 'react';
import { Inquiry } from '../types';
import { sendEmailClientSide } from '../utils/emailController';
import { Calculator, Sparkles, Send, ShieldCheck, CheckCircle2, History, Trash2 } from 'lucide-react';
interface ProjectEstimatorProps {
@@ -43,6 +44,20 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato
}
}, []);
const isMounted = useRef(false);
// Scroll to estimator top when step or success state changes
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true;
return;
}
const element = document.getElementById('estimator');
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, [step, success]);
// Compute estimate values
const calculateEstimate = () => {
// Base prices (Min, Max) in ZAR
@@ -197,24 +212,20 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato
onInquirySubmitted(newInquiry);
try {
await fetch('/api/send-email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name,
email,
phone,
company,
subject: estimate.serviceLabel,
message: notes || `Interactive estimate query for ${estimate.serviceLabel} (${estimate.scaleLabel}).`,
isEstimate: true,
estimateSummary: {
totalMin: estimate.totalMin,
totalMax: estimate.totalMax,
timelineWeeks: estimate.timelineWeeks,
breakdown: estimate.breakdown,
},
}),
await sendEmailClientSide({
name,
email,
phone,
company,
subject: estimate.serviceLabel,
message: notes || `Interactive estimate query for ${estimate.serviceLabel} (${estimate.scaleLabel}).`,
isEstimate: true,
estimateSummary: {
totalMin: estimate.totalMin,
totalMax: estimate.totalMax,
timelineWeeks: estimate.timelineWeeks,
breakdown: estimate.breakdown,
},
});
} catch (err) {
console.error('SMTP Proposal send failed. Saved locally:', err);
@@ -536,7 +547,7 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato
<span className="font-display text-3xl md:text-4xl font-extrabold text-brand-500">
R {estimate.totalMin.toLocaleString('en-ZA')}
</span>
<span className="text-slate-500 font-medium text-xs">to</span>
<span className="text-slate-400 font-medium text-xs">to</span>
<span className="font-display text-2xl md:text-3xl font-extrabold text-accent-400">
R {estimate.totalMax.toLocaleString('en-ZA')}
</span>
@@ -545,11 +556,11 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato
*Approximate cloud deployment, software engineering & layout rates. Scaled via khongisa.co.za Private Cloud.
</p>
</div>
{/* Key details */}
<div className="grid grid-cols-2 gap-4 py-4 border-y border-slate-800">
<div>
<p className="text-[9px] font-mono text-slate-500 uppercase tracking-wider">
<p className="text-[9px] font-mono text-slate-300 uppercase tracking-wider">
Completion Roadmap
</p>
<p className="font-bold text-sm text-white mt-1">
@@ -557,7 +568,7 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato
</p>
</div>
<div>
<p className="text-[9px] font-mono text-slate-500 uppercase tracking-wider">
<p className="text-[9px] font-mono text-slate-300 uppercase tracking-wider">
Priority Mode
</p>
<p className="font-bold text-sm text-brand-500 mt-1 capitalize">
+1 -13
View File
@@ -138,24 +138,12 @@ export const TIMELINE: TimelineEvent[] = [
},
{
year: '2022',
title: 'Multi-Platform Pivot',
description: 'Expanded our services to full web and desktop software layout design, securing major clients in Gauteng\'s logistics and retail sectors.',
milestone: false
},
{
year: '2023',
title: 'Private Cloud R&D',
description: 'In response to clients facing local server limitations, we began research into dedicated hypervisors, virtualization technologies, and private network clusters.',
milestone: false
},
{
year: '2024',
title: 'Khongisa Cloud Platform Launch',
description: 'Successfully built and rolled out our proprietary private cloud hosting platform (khongisa.co.za), allowing client applications to host seamlessly without third-party servers.',
milestone: true
},
{
year: '2025',
year: '2024',
title: 'Kubernetes & IaC Integration',
description: 'Expanded our operations to offer enterprise-grade Docker containerization, Kubernetes configuration blueprints, and multi-cloud Infrastructure as Code (IaC) designs.',
milestone: true
+8
View File
@@ -71,6 +71,14 @@
}
/* Base custom classes */
html, body {
font-family: var(--font-sans);
}
button, input, select, textarea {
font-family: inherit;
}
.font-display {
font-family: var(--font-display);
}
+147
View File
@@ -0,0 +1,147 @@
import { Inquiry } from '../types';
export interface EmailData {
name: string;
email: string;
phone?: string;
company?: string;
subject: string;
message: string;
isEstimate: boolean;
estimateSummary?: {
totalMin: number;
totalMax: number;
timelineWeeks: number;
breakdown: Array<{ item: string; price: string }>;
};
}
// Dynamically load the SMTPJS script to ensure it is always available
export function loadSmtpScript(): Promise<void> {
return Promise.resolve();
}
/**
* Handles sending emails directly via the external messenger API.
* This completely avoids the need for local SMTP secrets, PHP files, or internal proxying.
*/
export async function sendEmailClientSide(data: EmailData): Promise<{ success: boolean; message: string }> {
try {
const name = data.name.trim();
const email = data.email.trim();
const phone = data.phone?.trim() || 'Not Provided';
const company = data.company?.trim() || 'Not Provided';
const subject = data.subject?.trim() || 'General Inquiry';
const message = data.message?.trim() || 'No additional notes provided.';
const isEstimate = data.isEstimate;
const estimateSummary = data.estimateSummary;
// Generate Email Subject line
const emailSubject = isEstimate
? `LiteCharms Project Estimate Proposal: ${subject}`
: `LiteCharms General Service Inquiry: ${subject}`;
// Build breakdown items list for estimate summary
let breakdownHtml = '';
if (isEstimate && estimateSummary && Array.isArray(estimateSummary.breakdown)) {
breakdownHtml = estimateSummary.breakdown
.map(b => `<li>${b.item}: ${b.price}</li>`)
.join('');
}
// Compose professional HTML Email template
const emailHtml = `
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; color: #1e293b;">
<div style="background-color: #020617; padding: 24px; text-align: center; border-bottom: 2px solid #00bcac;">
<h2 style="color: #ffffff; margin: 0; font-size: 22px; letter-spacing: -0.02em;">Lite<span style="color: #00bcac;">Charms</span></h2>
<p style="color: #94a3b8; margin: 4px 0 0 0; font-size: 12px; font-family: monospace;">MIDRAND, SOUTH AFRICA</p>
</div>
<div style="padding: 24px; background-color: #ffffff;">
<h3 style="color: #00bcac; margin-top: 0; margin-bottom: 16px; font-size: 18px;">${isEstimate ? 'New Project Estimate Inquiry' : 'New General Service Inquiry'}</h3>
<table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; width: 120px; font-size: 13px; color: #64748b;">Client Name:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${name}</td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Email Address:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;"><a href="mailto:${email}">${email}</a></td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Phone Number:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${phone}</td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Company:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${company}</td>
</tr>
<tr style="border-bottom: 1px solid #f1f5f9;">
<td style="padding: 8px 0; font-weight: bold; font-size: 13px; color: #64748b;">Subject:</td>
<td style="padding: 8px 0; font-size: 13px; color: #1e293b;">${subject}</td>
</tr>
</table>
${isEstimate && estimateSummary ? `
<div style="background-color: #f8fafc; border: 1px dashed #cbd5e1; padding: 16px; border-radius: 6px; margin-bottom: 24px;">
<h4 style="margin-top: 0; color: #020617; font-size: 14px; border-bottom: 1px solid #e2e8f0; padding-bottom: 6px;">Selected Scope & Custom Pricing Range</h4>
<div style="margin-top: 8px; font-size: 13px;">
<strong>Estimated Range:</strong> <span style="color: #00bcac; font-weight: bold;">R ${estimateSummary.totalMin.toLocaleString('en-ZA')}</span> to <span style="color: #e11d48; font-weight: bold;">R ${estimateSummary.totalMax.toLocaleString('en-ZA')}</span>
<br/><strong>Roadmap:</strong> ~${estimateSummary.timelineWeeks} Weeks
</div>
${breakdownHtml ? `
<div style="margin-top: 12px; font-size: 11px; color: #64748b;">
<strong>Breakdown items:</strong>
<ul style="margin: 4px 0 0 0; padding-left: 16px;">
${breakdownHtml}
</ul>
</div>
` : ''}
</div>
` : ''}
<div style="margin-bottom: 24px;">
<h4 style="margin-top: 0; margin-bottom: 8px; font-size: 14px; color: #020617;">Client Message / Brief:</h4>
<p style="background-color: #f8fafc; padding: 12px; border-radius: 6px; font-size: 13px; line-height: 1.5; color: #334155; white-space: pre-wrap; margin: 0;">${message.replace(/\n/g, '<br/>')}</p>
</div>
</div>
<div style="background-color: #f1f5f9; padding: 16px; text-align: center; font-size: 11px; color: #64748b; border-top: 1px solid #e2e8f0;">
This is an automated dispatch from the LiteCharms website client.
</div>
</div>
`;
// Construct request body for messenger API
const requestBody = {
htmlBody: emailHtml,
from: 'contact@litecharms.co.za', // Default authorized sender from domain
to: 'contact@litecharms.co.za', // Recipient address
subject: emailSubject
};
const response = await fetch('https://messenger.api.khongisa.co.za/api/send-email', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(requestBody),
});
if (response.ok) {
return {
success: true,
message: 'Your inquiry has been submitted successfully! We will get in touch soon.',
};
} else {
const errorText = await response.text();
throw new Error(errorText || `Server responded with status ${response.status}`);
}
} catch (error: any) {
console.error('Messenger API send error:', error);
return {
success: false,
message: error?.message || 'Failed to dispatch email via Messenger API. Please verify your connection or email us directly at contact@litecharms.co.za',
};
}
}