API based email sender
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user