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> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
-2
View File
@@ -18,7 +18,6 @@
"express": "^4.21.2", "express": "^4.21.2",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"nodemailer": "^9.0.3",
"react": "^19.0.1", "react": "^19.0.1",
"react-dom": "^19.0.1", "react-dom": "^19.0.1",
"vite": "^6.2.3" "vite": "^6.2.3"
@@ -26,7 +25,6 @@
"devDependencies": { "devDependencies": {
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/nodemailer": "^8.0.1",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
+73 -120
View File
@@ -1,7 +1,6 @@
import express from 'express'; import express from 'express';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
import nodemailer from 'nodemailer';
import { createServer as createViteServer } from 'vite'; import { createServer as createViteServer } from 'vite';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
@@ -13,156 +12,110 @@ async function startServer() {
app.use(express.json()); app.use(express.json());
// API route to send email via SMTP // API route to send email via SMTP (supporting both standard and PHP endpoint pathways)
app.post('/api/send-email', async (req, res) => { app.post(['/api/send-email', '/send-email.php'], async (req, res) => {
const { name, email, phone, company, subject, message, isEstimate, estimateSummary } = req.body; const { name, email, phone, company, subject, message, isEstimate, estimateSummary } = req.body;
if (!name || !email) { if (!name || !email) {
return res.status(400).json({ error: 'Missing name or 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 // Generate Email Content
let emailSubject = subject || 'LiteCharms Inquiry'; const emailSubject = isEstimate
if (isEstimate) { ? `LiteCharms Project Estimate Proposal: ${subject}`
emailSubject = `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 = ` 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="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;"> <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> <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> <p style="color: #94a3b8; margin: 4px 0 0 0; font-size: 12px; font-family: monospace;">MIDRAND, SOUTH AFRICA</p>
</div> </div>
<div style="padding: 24px; background-color: #ffffff;"> <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> <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;"> <table style="width: 100%; border-collapse: collapse; margin-bottom: 24px;">
<tr style="border-bottom: 1px solid #f1f5f9;"> <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-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-size: 13px; color: #1e293b;">${name}</td>
</tr> </tr>
<tr style="border-bottom: 1px solid #f1f5f9;"> <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-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-size: 13px; color: #1e293b;"><a href="mailto:${email}">${email}</a></td>
</tr> </tr>
<tr style="border-bottom: 1px solid #f1f5f9;"> <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-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-size: 13px; color: #1e293b;">${phone || 'Not Provided'}</td>
</tr> </tr>
<tr style="border-bottom: 1px solid #f1f5f9;"> <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-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-size: 13px; color: #1e293b;">${company || 'Not Provided'}</td>
</tr> </tr>
<tr style="border-bottom: 1px solid #f1f5f9;"> <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-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-size: 13px; color: #1e293b;">${subject}</td>
</tr> </tr>
</table> </table>
`;
if (isEstimate && estimateSummary) { ${isEstimate && estimateSummary ? `
emailHtml += `
<div style="background-color: #f8fafc; border: 1px dashed #cbd5e1; padding: 16px; border-radius: 6px; margin-bottom: 24px;"> <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> <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;"> <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> <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 <br/><strong>Roadmap:</strong> ~${estimateSummary.timelineWeeks} Weeks
</div> </div>
<div style="margin-top: 12px; font-size: 11px; color: #64748b;"> ${breakdownHtml ? `
<strong>Breakdown items:</strong> <div style="margin-top: 12px; font-size: 11px; color: #64748b;">
<ul style="margin: 4px 0 0 0; padding-left: 16px;"> <strong>Breakdown items:</strong>
${(estimateSummary.breakdown || []).map((b: any) => `<li>${b.item}: ${b.price}</li>`).join('')} <ul style="margin: 4px 0 0 0; padding-left: 16px;">
</ul> ${breakdownHtml}
</div> </ul>
</div>
` : ''}
</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> <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> <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> </div>
<div style="background-color: #f1f5f9; padding: 16px; text-align: center; font-size: 11px; color: #64748b; border-top: 1px solid #e2e8f0;"> </div>
This is an automated dispatch from khongisa.co.za Private Cloud platform for LiteCharms (PTY) Ltd. <div style="background-color: #f1f5f9; padding: 16px; text-align: center; font-size: 11px; color: #64748b; border-top: 1px solid #e2e8f0;">
</div> This is an automated dispatch from the LiteCharms website server.
</div> </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
});
}
try { try {
const transporter = nodemailer.createTransport({ const response = await fetch('https://messenger.api.khongisa.co.za/api/send-email', {
host: smtpConfig.host, method: 'POST',
port: smtpConfig.port, headers: {
secure: smtpConfig.secure, 'Content-Type': 'application/json',
auth: {
user: smtpConfig.user,
pass: smtpConfig.pass,
}, },
body: JSON.stringify({
htmlBody: emailHtml,
from: 'contact@litecharms.co.za',
to: 'contact@litecharms.co.za',
subject: emailSubject,
}),
}); });
await transporter.sendMail({ if (response.ok) {
from: smtpConfig.fromEmail || smtpConfig.user, return res.json({ success: true, message: 'Email sent successfully via Messenger API' });
to: smtpConfig.toEmail, } else {
subject: emailSubject, const errorText = await response.text();
html: emailHtml, return res.status(500).json({ error: 'External API Error', details: errorText });
}); }
return res.json({ success: true, message: 'Email sent successfully via SMTP' });
} catch (error: any) { } catch (error: any) {
console.error('SMTP Email Send Error:', error); console.error('Server proxy email send error:', error);
return res.status(500).json({ return res.status(500).json({ error: 'Server error sending email', details: error.message });
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.'
});
} }
}); });
-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 AuroraBackground from './components/AuroraBackground';
import { BrandIcon, BrandLogo } from './components/BrandAssets'; import { BrandIcon, BrandLogo } from './components/BrandAssets';
import { Inquiry } from './types'; import { Inquiry } from './types';
import { sendEmailClientSide } from './utils/emailController';
import { import {
MapPin, MapPin,
Mail, Mail,
@@ -28,8 +29,8 @@ export default function App() {
const [contactLoading, setContactLoading] = useState(false); const [contactLoading, setContactLoading] = useState(false);
// Stats Counters state for load-in effect // Stats Counters state for load-in effect
const [activeClients, setActiveClients] = useState(12); const [activeClients, setActiveClients] = useState(1);
const [completedProjects, setCompletedProjects] = useState(25); const [completedProjects, setCompletedProjects] = useState(5);
useEffect(() => { useEffect(() => {
// Soft animate stats for visual delight // Soft animate stats for visual delight
@@ -39,9 +40,9 @@ export default function App() {
clearInterval(clientsTimer); clearInterval(clientsTimer);
return 4; return 4;
} }
return prev + 2; return prev + 1;
}); });
}, 40); }, 150);
const projectsTimer = setInterval(() => { const projectsTimer = setInterval(() => {
setCompletedProjects((prev) => { setCompletedProjects((prev) => {
@@ -49,9 +50,9 @@ export default function App() {
clearInterval(projectsTimer); clearInterval(projectsTimer);
return 25; return 25;
} }
return prev + 4; return prev + 1;
}); });
}, 25); }, 40);
return () => { return () => {
clearInterval(clientsTimer); clearInterval(clientsTimer);
@@ -93,19 +94,15 @@ export default function App() {
localStorage.setItem('litecharms_general_contacts', JSON.stringify(pastMsgs)); localStorage.setItem('litecharms_general_contacts', JSON.stringify(pastMsgs));
try { try {
await fetch('/api/send-email', { await sendEmailClientSide({
method: 'POST', name: contactName,
headers: { 'Content-Type': 'application/json' }, email: contactEmail,
body: JSON.stringify({ subject: contactSubject,
name: contactName, message: contactMessage,
email: contactEmail, isEstimate: false,
subject: contactSubject,
message: contactMessage,
isEstimate: false,
}),
}); });
} catch (err) { } catch (err) {
console.error('SMTP API Submission failed. Saved locally instead:', err); console.error('SMTP direct send failed. Saved locally instead:', err);
} finally { } finally {
setContactLoading(false); setContactLoading(false);
setContactSuccess(true); setContactSuccess(true);
@@ -267,10 +264,10 @@ export default function App() {
</h2> </h2>
<div className="space-y-4 text-sm leading-relaxed text-slate-300"> <div className="space-y-4 text-sm leading-relaxed text-slate-300">
<p> <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>
<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>
<p className="font-medium text-white"> <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. 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="pt-6 border-t border-slate-800 space-y-3 text-xs">
<div className="flex items-center space-x-3 text-slate-300"> <div className="flex items-center space-x-3 text-slate-300">
<MapPin className="w-4 h-4 text-brand-500 shrink-0" /> <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>
<div className="flex items-center space-x-3 text-slate-300"> <div className="flex items-center space-x-3 text-slate-300">
<Clock className="w-4 h-4 text-brand-500 shrink-0" /> <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" 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"> <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> <span className="text-sm md:text-base leading-snug">{faq.question}</span>
</div> </div>
<ChevronDown <ChevronDown
className={`w-4 h-4 text-slate-500 shrink-0 transition-transform duration-300 ${ className={`w-4 h-4 text-slate-400 shrink-0 transition-transform duration-300 ${
isOpen ? 'rotate-180 text-brand-500' : '' isOpen ? 'rotate-180 text-brand-400' : ''
}`} }`}
/> />
</button> </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 { Inquiry } from '../types';
import { sendEmailClientSide } from '../utils/emailController';
import { Calculator, Sparkles, Send, ShieldCheck, CheckCircle2, History, Trash2 } from 'lucide-react'; import { Calculator, Sparkles, Send, ShieldCheck, CheckCircle2, History, Trash2 } from 'lucide-react';
interface ProjectEstimatorProps { 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 // Compute estimate values
const calculateEstimate = () => { const calculateEstimate = () => {
// Base prices (Min, Max) in ZAR // Base prices (Min, Max) in ZAR
@@ -197,24 +212,20 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato
onInquirySubmitted(newInquiry); onInquirySubmitted(newInquiry);
try { try {
await fetch('/api/send-email', { await sendEmailClientSide({
method: 'POST', name,
headers: { 'Content-Type': 'application/json' }, email,
body: JSON.stringify({ phone,
name, company,
email, subject: estimate.serviceLabel,
phone, message: notes || `Interactive estimate query for ${estimate.serviceLabel} (${estimate.scaleLabel}).`,
company, isEstimate: true,
subject: estimate.serviceLabel, estimateSummary: {
message: notes || `Interactive estimate query for ${estimate.serviceLabel} (${estimate.scaleLabel}).`, totalMin: estimate.totalMin,
isEstimate: true, totalMax: estimate.totalMax,
estimateSummary: { timelineWeeks: estimate.timelineWeeks,
totalMin: estimate.totalMin, breakdown: estimate.breakdown,
totalMax: estimate.totalMax, },
timelineWeeks: estimate.timelineWeeks,
breakdown: estimate.breakdown,
},
}),
}); });
} catch (err) { } catch (err) {
console.error('SMTP Proposal send failed. Saved locally:', 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"> <span className="font-display text-3xl md:text-4xl font-extrabold text-brand-500">
R {estimate.totalMin.toLocaleString('en-ZA')} R {estimate.totalMin.toLocaleString('en-ZA')}
</span> </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"> <span className="font-display text-2xl md:text-3xl font-extrabold text-accent-400">
R {estimate.totalMax.toLocaleString('en-ZA')} R {estimate.totalMax.toLocaleString('en-ZA')}
</span> </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. *Approximate cloud deployment, software engineering & layout rates. Scaled via khongisa.co.za Private Cloud.
</p> </p>
</div> </div>
{/* Key details */} {/* Key details */}
<div className="grid grid-cols-2 gap-4 py-4 border-y border-slate-800"> <div className="grid grid-cols-2 gap-4 py-4 border-y border-slate-800">
<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">
Completion Roadmap Completion Roadmap
</p> </p>
<p className="font-bold text-sm text-white mt-1"> <p className="font-bold text-sm text-white mt-1">
@@ -557,7 +568,7 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato
</p> </p>
</div> </div>
<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 Priority Mode
</p> </p>
<p className="font-bold text-sm text-brand-500 mt-1 capitalize"> <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', 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', 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.', 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 milestone: true
}, },
{ {
year: '2025', year: '2024',
title: 'Kubernetes & IaC Integration', 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.', description: 'Expanded our operations to offer enterprise-grade Docker containerization, Kubernetes configuration blueprints, and multi-cloud Infrastructure as Code (IaC) designs.',
milestone: true milestone: true
+8
View File
@@ -71,6 +71,14 @@
} }
/* Base custom classes */ /* Base custom classes */
html, body {
font-family: var(--font-sans);
}
button, input, select, textarea {
font-family: inherit;
}
.font-display { .font-display {
font-family: var(--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',
};
}
}