diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..7a550fe --- /dev/null +++ b/.env.example @@ -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" diff --git a/index.html b/index.html index ae72e37..942edd3 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,9 @@ - LiteCharms IT Services + Lite Charms + +
diff --git a/package.json b/package.json index 67433b2..22f336c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server.ts b/server.ts index 10da381..59e6873 100644 --- a/server.ts +++ b/server.ts @@ -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) => `
  • ${b.item}: ${b.price}
  • `) + .join(''); } - let emailHtml = ` -
    -
    -

    LiteCharms

    -

    MIDRAND, SOUTH AFRICA

    -
    -
    -

    ${isEstimate ? 'New Project Estimate Inquiry' : 'New General Service Inquiry'}

    - - + const emailHtml = ` +
    +
    +

    LiteCharms

    +

    MIDRAND, SOUTH AFRICA

    +
    +
    +

    ${isEstimate ? 'New Project Estimate Inquiry' : 'New General Service Inquiry'}

    + +
    - - + + - - + + - - + + - - + + - - + + -
    Client Name:${name}Client Name:${name}
    Email Address:${email}Email Address:${email}
    Phone Number:${phone || 'Not Provided'}Phone Number:${phone || 'Not Provided'}
    Company:${company || 'Not Provided'}Company:${company || 'Not Provided'}
    Subject:${subject || 'General Service Contact'}Subject:${subject}
    - `; + - if (isEstimate && estimateSummary) { - emailHtml += ` + ${isEstimate && estimateSummary ? `
    -

    Selected Scope & Custom Pricing Range

    -
    - Estimated Range: R ${estimateSummary.totalMin?.toLocaleString('en-ZA')} to R ${estimateSummary.totalMax?.toLocaleString('en-ZA')} -
    Roadmap: ~${estimateSummary.timelineWeeks} Weeks -
    -
    - Breakdown items: -
      - ${(estimateSummary.breakdown || []).map((b: any) => `
    • ${b.item}: ${b.price}
    • `).join('')} -
    -
    +

    Selected Scope & Custom Pricing Range

    +
    + Estimated Range: R ${estimateSummary.totalMin?.toLocaleString('en-ZA')} to R ${estimateSummary.totalMax?.toLocaleString('en-ZA')} +
    Roadmap: ~${estimateSummary.timelineWeeks} Weeks +
    + ${breakdownHtml ? ` +
    + Breakdown items: +
      + ${breakdownHtml} +
    +
    + ` : ''}
    - `; - } + ` : ''} - emailHtml += ` -
    +

    Client Message / Brief:

    -

    ${message || 'No additional notes provided.'}

    -
    +

    ${(message || 'No additional notes provided.').replace(/\n/g, '
    ')}

    -
    - This is an automated dispatch from khongisa.co.za Private Cloud platform for LiteCharms (PTY) Ltd. -
    -
    - `; - - // 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 - }); - } +
    +
    + This is an automated dispatch from the LiteCharms website server. +
    + +`; 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 }); } }); diff --git a/smtp-config.json b/smtp-config.json deleted file mode 100644 index eee4802..0000000 --- a/smtp-config.json +++ /dev/null @@ -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 ", - "toEmail": "contact@litecharms.co.za" -} diff --git a/src/App.tsx b/src/App.tsx index e6ea581..56dd8dd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() {

    - 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.

    - 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.

    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() {

    - 1633 Liebenburg Rd, Noordwyk, Midrand, Gauteng, ZA + 1633 Liebenburg Rd, Noordwyk, Midrand, 1687, South Africa
    diff --git a/src/components/FAQSection.tsx b/src/components/FAQSection.tsx index 4a181d1..f045780 100644 --- a/src/components/FAQSection.tsx +++ b/src/components/FAQSection.tsx @@ -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" >
    - + {faq.question}
    diff --git a/src/components/ProjectEstimator.tsx b/src/components/ProjectEstimator.tsx index 187ac2f..1d9423a 100644 --- a/src/components/ProjectEstimator.tsx +++ b/src/components/ProjectEstimator.tsx @@ -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 R {estimate.totalMin.toLocaleString('en-ZA')} - to + to R {estimate.totalMax.toLocaleString('en-ZA')} @@ -545,11 +556,11 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato *Approximate cloud deployment, software engineering & layout rates. Scaled via khongisa.co.za Private Cloud.

    - + {/* Key details */}
    -

    +

    Completion Roadmap

    @@ -557,7 +568,7 @@ export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimato

    -

    +

    Priority Mode

    diff --git a/src/data.ts b/src/data.ts index 1b161b6..21eb533 100644 --- a/src/data.ts +++ b/src/data.ts @@ -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 diff --git a/src/index.css b/src/index.css index 62bad8c..b7d920d 100644 --- a/src/index.css +++ b/src/index.css @@ -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); } diff --git a/src/utils/emailController.ts b/src/utils/emailController.ts new file mode 100644 index 0000000..dc334aa --- /dev/null +++ b/src/utils/emailController.ts @@ -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 { + 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 => `

  • ${b.item}: ${b.price}
  • `) + .join(''); + } + + // Compose professional HTML Email template + const emailHtml = ` +
    +
    +

    LiteCharms

    +

    MIDRAND, SOUTH AFRICA

    +
    +
    +

    ${isEstimate ? 'New Project Estimate Inquiry' : 'New General Service Inquiry'}

    + + + + + + + + + + + + + + + + + + + + + + +
    Client Name:${name}
    Email Address:${email}
    Phone Number:${phone}
    Company:${company}
    Subject:${subject}
    + + ${isEstimate && estimateSummary ? ` +
    +

    Selected Scope & Custom Pricing Range

    +
    + Estimated Range: R ${estimateSummary.totalMin.toLocaleString('en-ZA')} to R ${estimateSummary.totalMax.toLocaleString('en-ZA')} +
    Roadmap: ~${estimateSummary.timelineWeeks} Weeks +
    + ${breakdownHtml ? ` +
    + Breakdown items: +
      + ${breakdownHtml} +
    +
    + ` : ''} +
    + ` : ''} + +
    +

    Client Message / Brief:

    +

    ${message.replace(/\n/g, '
    ')}

    +
    +
    +
    + This is an automated dispatch from the LiteCharms website client. +
    +
    +`; + + // 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', + }; + } +} +