first commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
build/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
*.log
|
||||
.env*
|
||||
!.env.example
|
||||
@@ -0,0 +1,20 @@
|
||||
<div align="center">
|
||||
<img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
|
||||
</div>
|
||||
|
||||
# Run and deploy your AI Studio app
|
||||
|
||||
This contains everything you need to run your app locally.
|
||||
|
||||
View your app in AI Studio: https://ai.studio/apps/87fc9c98-0670-4302-bdb3-5bb0fd1e9ad2
|
||||
|
||||
## Run Locally
|
||||
|
||||
**Prerequisites:** Node.js
|
||||
|
||||
|
||||
1. Install dependencies:
|
||||
`npm install`
|
||||
2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
|
||||
3. Run the app:
|
||||
`npm run dev`
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>LiteCharms IT Services</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "LiteCharms IT Services",
|
||||
"description": "Business profile for LiteCharms (PTY) Ltd, based in Midrand, South Africa. Specializing in web, desktop, and mobile app design, and IT infrastructure planning, design, and rollout.",
|
||||
"requestFramePermissions": [],
|
||||
"majorCapabilities": ["MAJOR_CAPABILITY_SERVER_SIDE_GEMINI_API"]
|
||||
}
|
||||
Generated
+4299
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "react-example",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx server.ts",
|
||||
"build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
|
||||
"start": "node dist/server.cjs",
|
||||
"clean": "rm -rf dist server.js",
|
||||
"lint": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "^2.4.0",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@vitejs/plugin-react": "^5.0.4",
|
||||
"dotenv": "^17.2.3",
|
||||
"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"
|
||||
},
|
||||
"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",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "~5.8.2",
|
||||
"vite": "^6.2.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
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';
|
||||
|
||||
dotenv.config();
|
||||
|
||||
async function startServer() {
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
// API route to send email via SMTP
|
||||
app.post('/api/send-email', 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}`;
|
||||
}
|
||||
|
||||
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;">
|
||||
<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 || '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>
|
||||
</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>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
|
||||
if (isEstimate && estimateSummary) {
|
||||
emailHtml += `
|
||||
<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>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
emailHtml += `
|
||||
<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>
|
||||
</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
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpConfig.host,
|
||||
port: smtpConfig.port,
|
||||
secure: smtpConfig.secure,
|
||||
auth: {
|
||||
user: smtpConfig.user,
|
||||
pass: smtpConfig.pass,
|
||||
},
|
||||
});
|
||||
|
||||
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' });
|
||||
} 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.'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Vite middleware setup
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
const vite = await createViteServer({
|
||||
server: { middlewareMode: true },
|
||||
appType: 'spa',
|
||||
});
|
||||
app.use(vite.middlewares);
|
||||
} else {
|
||||
const distPath = path.join(process.cwd(), 'dist');
|
||||
app.use(express.static(distPath));
|
||||
app.get('*', (req, res) => {
|
||||
res.sendFile(path.join(distPath, 'index.html'));
|
||||
});
|
||||
}
|
||||
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
console.log(`LiteCharms full-stack application running on http://localhost:${PORT}`);
|
||||
});
|
||||
}
|
||||
|
||||
startServer();
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
+661
@@ -0,0 +1,661 @@
|
||||
import { useState, useEffect, FormEvent } from 'react';
|
||||
import Header from './components/Header';
|
||||
import ServiceExplorer from './components/ServiceExplorer';
|
||||
import ProjectEstimator from './components/ProjectEstimator';
|
||||
import CompanyTimeline from './components/CompanyTimeline';
|
||||
import FAQSection from './components/FAQSection';
|
||||
import AuroraBackground from './components/AuroraBackground';
|
||||
import { BrandIcon, BrandLogo } from './components/BrandAssets';
|
||||
import { Inquiry } from './types';
|
||||
import {
|
||||
MapPin,
|
||||
Mail,
|
||||
Phone,
|
||||
Clock,
|
||||
Send,
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
Sparkles
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function App() {
|
||||
// General Contact Form States
|
||||
const [contactName, setContactName] = useState('');
|
||||
const [contactEmail, setContactEmail] = useState('');
|
||||
const [contactSubject, setContactSubject] = useState('General Services Inquiry');
|
||||
const [contactMessage, setContactMessage] = useState('');
|
||||
const [contactSuccess, setContactSuccess] = useState(false);
|
||||
const [contactLoading, setContactLoading] = useState(false);
|
||||
|
||||
// Stats Counters state for load-in effect
|
||||
const [activeClients, setActiveClients] = useState(12);
|
||||
const [completedProjects, setCompletedProjects] = useState(25);
|
||||
|
||||
useEffect(() => {
|
||||
// Soft animate stats for visual delight
|
||||
const clientsTimer = setInterval(() => {
|
||||
setActiveClients((prev) => {
|
||||
if (prev >= 65) {
|
||||
clearInterval(clientsTimer);
|
||||
return 65;
|
||||
}
|
||||
return prev + 2;
|
||||
});
|
||||
}, 40);
|
||||
|
||||
const projectsTimer = setInterval(() => {
|
||||
setCompletedProjects((prev) => {
|
||||
if (prev >= 142) {
|
||||
clearInterval(projectsTimer);
|
||||
return 142;
|
||||
}
|
||||
return prev + 4;
|
||||
});
|
||||
}, 25);
|
||||
|
||||
return () => {
|
||||
clearInterval(clientsTimer);
|
||||
clearInterval(projectsTimer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const scrollToSection = (id: string) => {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
const headerOffset = 80;
|
||||
const elementPosition = element.getBoundingClientRect().top + window.scrollY;
|
||||
const offsetPosition = elementPosition - headerOffset;
|
||||
window.scrollTo({
|
||||
top: offsetPosition,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleGeneralContactSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!contactName || !contactEmail || !contactMessage) return;
|
||||
|
||||
setContactLoading(true);
|
||||
|
||||
// Save general inquiry to localStorage
|
||||
const savedMsg = {
|
||||
id: `contact-${Date.now()}`,
|
||||
name: contactName,
|
||||
email: contactEmail,
|
||||
subject: contactSubject,
|
||||
message: contactMessage,
|
||||
date: new Date().toLocaleDateString('en-ZA'),
|
||||
};
|
||||
|
||||
const pastMsgs = JSON.parse(localStorage.getItem('litecharms_general_contacts') || '[]');
|
||||
pastMsgs.push(savedMsg);
|
||||
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,
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('SMTP API Submission failed. Saved locally instead:', err);
|
||||
} finally {
|
||||
setContactLoading(false);
|
||||
setContactSuccess(true);
|
||||
setContactName('');
|
||||
setContactEmail('');
|
||||
setContactMessage('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEstimatorInquiry = (inquiry: Inquiry) => {
|
||||
console.log('Received Estimate Inquiry:', inquiry);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-brand-950 font-sans text-slate-300 antialiased relative z-10 selection:bg-brand-500/30 selection:text-white" id="app-root">
|
||||
{/* Aurora Background */}
|
||||
<AuroraBackground />
|
||||
|
||||
{/* Navigation Header */}
|
||||
<Header onScrollTo={scrollToSection} />
|
||||
|
||||
{/* 1. Hero Section */}
|
||||
<section
|
||||
id="hero"
|
||||
className="pt-32 pb-20 md:pt-40 md:pb-28 relative overflow-hidden"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6 relative">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
|
||||
{/* Hero Left Content */}
|
||||
<div className="lg:col-span-7 space-y-8 text-left">
|
||||
{/* South Africa Tag */}
|
||||
<div className="inline-flex items-center space-x-2 bg-brand-900/50 border border-brand-800/80 px-3.5 py-1.5 rounded-full" id="hero-sa-tag">
|
||||
<span className="w-2 h-2 rounded-full bg-brand-500 animate-pulse" />
|
||||
<span className="text-[11px] font-mono font-bold uppercase tracking-wider text-brand-100">
|
||||
Established 2011 • Midrand, South Africa
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Display Headline */}
|
||||
<h1 className="font-display text-4xl sm:text-5xl lg:text-6xl font-extrabold tracking-tight text-white leading-[1.15]" id="hero-headline">
|
||||
Bespoke App Design & <br />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-brand-500 via-brand-200 to-accent-400">
|
||||
Cloud Deployments
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
{/* Narrative Subhead */}
|
||||
<p className="text-base sm:text-lg text-slate-300 leading-relaxed max-w-2xl" id="hero-description">
|
||||
LiteCharms (PTY) Ltd provides an integrated approach to software layouts and modern deployment solutions.
|
||||
We engineer immersive interfaces for web, mobile, and desktop systems, alongside Docker containerization, Kubernetes configurations, and cloud hosting for clients who do not have hosting environments.
|
||||
</p>
|
||||
|
||||
{/* CTAs */}
|
||||
<div className="flex flex-col sm:flex-row items-stretch sm:items-center gap-4 pt-2">
|
||||
<button
|
||||
id="hero-cta-estimator"
|
||||
onClick={() => scrollToSection('estimator')}
|
||||
className="px-6 py-3.5 rounded-xl font-bold text-sm text-white bg-brand-500 hover:bg-brand-600 shadow-lg shadow-brand-500/20 transition-all text-center flex items-center justify-center space-x-2 cursor-pointer hover:scale-[1.02] duration-250"
|
||||
>
|
||||
<span>Build Interactive Estimate</span>
|
||||
<Sparkles className="w-4 h-4 text-brand-100" />
|
||||
</button>
|
||||
<button
|
||||
id="hero-cta-services"
|
||||
onClick={() => scrollToSection('services')}
|
||||
className="px-6 py-3.5 rounded-xl font-bold text-sm text-slate-200 bg-slate-900/60 hover:bg-slate-900 border border-slate-800 hover:border-slate-700 transition-all text-center flex items-center justify-center space-x-2 cursor-pointer hover:scale-[1.02] duration-250"
|
||||
>
|
||||
<span>Explore Our Services</span>
|
||||
<ArrowRight className="w-4 h-4 text-slate-400" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Deployment Disclaimer Badge */}
|
||||
<div className="p-4 rounded-xl bg-slate-900/50 backdrop-blur-md border border-slate-800/80 flex items-start space-x-3 text-xs text-slate-400 max-w-xl">
|
||||
<div className="w-5 h-5 rounded-md bg-brand-950 border border-brand-800/50 flex items-center justify-center text-brand-500 font-bold shrink-0 text-[10px] mt-0.5 font-mono">IaC</div>
|
||||
<p className="leading-relaxed">
|
||||
<strong>Cloud & Container Ready:</strong> Built to compile as a lightweight optimized single-page layout, ready to deploy flawlessly inside containerized Docker environments or on our khongisa.co.za private cloud.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Hero Right Visuals - Bento grid style */}
|
||||
<div className="lg:col-span-5 grid grid-cols-2 gap-4 relative" id="hero-visual-bento">
|
||||
{/* Card 1: Main Stats Box */}
|
||||
<div className="bg-slate-900/50 backdrop-blur-md rounded-2xl p-6 border border-slate-800/80 shadow-sm col-span-2 hover:border-slate-700 transition-all">
|
||||
<p className="text-[10px] font-mono text-slate-400 uppercase tracking-widest">
|
||||
Our Engineering Track Record
|
||||
</p>
|
||||
<div className="grid grid-cols-3 gap-4 mt-4">
|
||||
<div>
|
||||
<p className="text-2xl md:text-3xl font-extrabold font-display text-brand-500">
|
||||
2011
|
||||
</p>
|
||||
<p className="text-[10px] font-medium text-slate-400 uppercase mt-0.5">
|
||||
Established
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl md:text-3xl font-extrabold font-display text-white">
|
||||
{completedProjects}+
|
||||
</p>
|
||||
<p className="text-[10px] font-medium text-slate-400 uppercase mt-0.5">
|
||||
Projects
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-2xl md:text-3xl font-extrabold font-display text-white">
|
||||
{activeClients}+
|
||||
</p>
|
||||
<p className="text-[10px] font-medium text-slate-400 uppercase mt-0.5">
|
||||
Active Clients
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Card 2: Core Focus A */}
|
||||
<div className="bg-gradient-to-br from-brand-600 to-brand-850 rounded-2xl p-6 text-white shadow-md shadow-brand-500/5 border border-brand-500/20">
|
||||
<div className="w-9 h-9 rounded-lg bg-white/20 flex items-center justify-center mb-4">
|
||||
<BrandIcon className="w-5 h-5 text-brand-100" />
|
||||
</div>
|
||||
<h4 className="font-display font-bold text-sm leading-tight">
|
||||
Responsive App Layouts
|
||||
</h4>
|
||||
<p className="text-[10px] text-brand-100 mt-2 leading-relaxed">
|
||||
Wireframes and structural guidelines for web dashboards, native iOS/Android, and high-productivity desktop software.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Card 3: Core Focus B */}
|
||||
<div className="bg-slate-900/80 backdrop-blur-md rounded-2xl p-6 text-slate-100 shadow-md border border-slate-800/80">
|
||||
<div className="w-9 h-9 rounded-lg bg-slate-800 flex items-center justify-center mb-4 border border-slate-700">
|
||||
<span className="text-brand-500 text-xs font-mono font-bold">IaC</span>
|
||||
</div>
|
||||
<h4 className="font-display font-bold text-sm leading-tight text-white">
|
||||
Docker & Kubernetes
|
||||
</h4>
|
||||
<p className="text-[10px] text-slate-400 mt-2 leading-relaxed">
|
||||
Declarative Kubernetes cluster blueprints, Docker multi-stage configurations, and cloud setups on khongisa.co.za.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 2. About Section & Timeline */}
|
||||
<section id="about" className="py-24 sm:py-32 relative">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
|
||||
{/* Left side text */}
|
||||
<div className="lg:col-span-5 space-y-6 lg:sticky lg:top-28">
|
||||
<span className="text-xs font-mono font-bold uppercase tracking-wider text-brand-100 bg-brand-900/50 px-3 py-1.5 rounded-md border border-brand-800/60 inline-block">
|
||||
Our Heritage
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
Established in Midrand. <br />
|
||||
Sustained by Engineering.
|
||||
</h2>
|
||||
<div className="space-y-4 text-sm leading-relaxed text-slate-300">
|
||||
<p>
|
||||
LiteCharms (PTY) Ltd was born in Midrand in 2011, 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 2017, 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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Physical Address details widget */}
|
||||
<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>Grand Central Office Park, Midrand, Johannesburg, 1685, 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" />
|
||||
<span>Monday - Friday: 08:00 - 17:00 (GMT+2)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right side interactive timeline */}
|
||||
<div className="lg:col-span-7">
|
||||
<CompanyTimeline />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. Services Section */}
|
||||
<section id="services" className="py-24 sm:py-32 relative border-y border-slate-900 bg-slate-950/20 backdrop-blur-[2px]">
|
||||
<div className="max-w-7xl mx-auto px-6 text-center space-y-12">
|
||||
<div className="max-w-3xl mx-auto space-y-4">
|
||||
<span className="text-xs font-mono font-bold uppercase tracking-wider text-brand-100 bg-brand-900/50 px-3 py-1.5 rounded-md border border-brand-800/60 inline-block">
|
||||
Full Suite Solutions
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
Bespoke Digital Design & Cloud Deployments
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-slate-300 leading-relaxed">
|
||||
We separate our specialties into cohesive pillars. Explore our capabilities to see how we plan, design, and roll out interfaces and infrastructure.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Service Explorer Accordion / Tabs */}
|
||||
<ServiceExplorer />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 4. Project Estimator Section */}
|
||||
<section id="estimator" className="py-24 sm:py-32 relative">
|
||||
<div className="max-w-7xl mx-auto px-6 space-y-12">
|
||||
<div className="text-center max-w-3xl mx-auto space-y-4">
|
||||
<span className="text-xs font-mono font-bold uppercase tracking-wider text-brand-100 bg-brand-900/50 px-3 py-1.5 rounded-md border border-brand-800/60 inline-block">
|
||||
Interactive Estimator
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
Model Your Custom Project Scope
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-slate-300 leading-relaxed">
|
||||
Select your required app layout services or cloud hosting parameters. Our system computes an investment range in South African Rands (ZAR) instantly. Submit the configuration to lock in your draft!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ProjectEstimator onInquirySubmitted={handleEstimatorInquiry} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 5. FAQs Section */}
|
||||
<section id="faqs" className="py-24 sm:py-32 border-t border-slate-900 bg-slate-950/30 backdrop-blur-[2px]">
|
||||
<div className="max-w-7xl mx-auto px-6 space-y-12">
|
||||
<div className="text-center max-w-3xl mx-auto space-y-4">
|
||||
<span className="text-xs font-mono font-bold uppercase tracking-wider text-brand-100 bg-brand-900/50 px-3 py-1.5 rounded-md border border-brand-800/60 inline-block">
|
||||
Support Center
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
Frequently Answered Questions
|
||||
</h2>
|
||||
<p className="text-sm sm:text-base text-slate-300 leading-relaxed">
|
||||
Find instant answers regarding our site planning timelines, khongisa.co.za cloud hosting options, Gauteng region operations, and modern cloud deployment configurations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<FAQSection />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 6. Contact & Directions Section */}
|
||||
<section id="contact" className="py-24 sm:py-32 relative">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-16 items-start">
|
||||
{/* Contact Info Panel */}
|
||||
<div className="lg:col-span-5 space-y-8" id="contact-info-panel">
|
||||
<div className="space-y-4">
|
||||
<span className="text-xs font-mono font-bold uppercase tracking-wider text-brand-100 bg-brand-900/50 px-3 py-1.5 rounded-md border border-brand-800/60 inline-block">
|
||||
Get In Touch
|
||||
</span>
|
||||
<h2 className="font-display text-3xl sm:text-4xl font-extrabold text-white tracking-tight">
|
||||
Reach Our Midrand Office
|
||||
</h2>
|
||||
<p className="text-sm leading-relaxed text-slate-300">
|
||||
Ready to map out your infrastructure or design high-performance software layouts? Contact our directors directly or submit the general inquiry form. You can also visit our offices in Gauteng.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Direct Info list */}
|
||||
<div className="space-y-4" id="direct-contact-links">
|
||||
<a
|
||||
href="mailto:contact@litecharms.co.za"
|
||||
id="contact-email-link"
|
||||
className="flex items-center space-x-4 p-4 rounded-xl border border-slate-800/80 hover:border-slate-700 bg-slate-900/20 hover:bg-slate-900/40 transition-all text-slate-300 hover:text-white"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-brand-900/60 flex items-center justify-center text-brand-500">
|
||||
<Mail className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-mono font-semibold text-slate-500 uppercase">Email Us</p>
|
||||
<p className="text-sm font-bold">contact@litecharms.co.za</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="tel:+27872659463"
|
||||
id="contact-phone-link"
|
||||
className="flex items-center space-x-4 p-4 rounded-xl border border-slate-800/80 hover:border-slate-700 bg-slate-900/20 hover:bg-slate-900/40 transition-all text-slate-300 hover:text-white"
|
||||
>
|
||||
<div className="w-10 h-10 rounded-lg bg-brand-900/60 flex items-center justify-center text-brand-500">
|
||||
<Phone className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-mono font-semibold text-slate-500 uppercase">Call Office</p>
|
||||
<p className="text-sm font-bold">+27 (0) 87 265 9463</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div className="flex items-center space-x-4 p-4 rounded-xl border border-slate-800/80 bg-slate-900/20 text-slate-300">
|
||||
<div className="w-10 h-10 rounded-lg bg-brand-900/60 flex items-center justify-center text-brand-500">
|
||||
<Clock className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] font-mono font-semibold text-slate-500 uppercase">Core Office Hours</p>
|
||||
<p className="text-sm font-bold">Mon - Fri: 08:00 - 17:00</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Graphic Midrand South Africa Map Representation */}
|
||||
<div className="p-6 bg-slate-900/40 backdrop-blur-sm rounded-2xl border border-slate-800/80 space-y-4" id="map-visual-block">
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h4 className="font-display font-bold text-sm text-white">Midrand Headquarters</h4>
|
||||
<p className="text-xs text-slate-400 mt-1">1633 Liebenburg Rd, Boordwyk, Midrand, Gauteng, ZA</p>
|
||||
</div>
|
||||
<span className="text-[9px] font-mono font-bold uppercase bg-slate-800 text-slate-300 px-2 py-0.5 rounded border border-slate-700 shrink-0">
|
||||
Gauteng Corridor
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Styled geometric CSS map visual */}
|
||||
<div className="h-32 bg-brand-950 rounded-xl relative overflow-hidden border border-slate-800 flex items-center justify-center">
|
||||
{/* Grid background lines */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(#00bcac_2px,transparent_2px)] [background-size:20px_20px] opacity-15" />
|
||||
<div className="absolute top-1/2 w-full h-0.5 bg-brand-500/20" />
|
||||
<div className="absolute left-1/3 h-full w-0.5 bg-brand-500/20" />
|
||||
<div className="absolute right-1/4 h-full w-0.5 bg-brand-500/20" />
|
||||
|
||||
{/* Johannesburg - Pretoria corridor labels */}
|
||||
<span className="absolute top-3 left-4 text-[9px] font-mono text-brand-200">Pretoria (N1 North)</span>
|
||||
<span className="absolute bottom-3 left-4 text-[9px] font-mono text-brand-200">Johannesburg (N1 South)</span>
|
||||
|
||||
{/* Pulsing local coordinate pointer */}
|
||||
<div className="relative z-10 flex flex-col items-center">
|
||||
<div className="w-3 h-3 bg-brand-500 rounded-full animate-ping absolute" />
|
||||
<div className="w-3 h-3 bg-brand-500 rounded-full border-2 border-white relative z-10" />
|
||||
<span className="text-[10px] font-bold text-white bg-slate-950/90 backdrop-blur-sm px-2 py-0.5 rounded border border-slate-800 mt-1.5 shadow-md">
|
||||
LiteCharms Offices
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* General Contact Form */}
|
||||
<div className="lg:col-span-7 bg-slate-900/30 backdrop-blur-md border border-slate-800/80 p-8 rounded-2xl shadow-sm" id="contact-form-panel">
|
||||
<div className="border-b border-slate-800 pb-5 mb-6">
|
||||
<h3 className="font-display text-xl font-bold text-white">General Message Form</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Have a general service query or want to request a hardcopy portfolio handover? Leave your request below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{contactSuccess ? (
|
||||
<div className="p-8 text-center bg-slate-900/50 rounded-xl border border-brand-500/30 space-y-4 animate-fade-in" id="contact-success-box">
|
||||
<div className="w-12 h-12 bg-brand-900/50 border border-brand-500/30 text-brand-500 rounded-full flex items-center justify-center mx-auto shadow-sm">
|
||||
<CheckCircle2 className="w-6 h-6" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-display font-bold text-lg text-white">Message Delivered!</h4>
|
||||
<p className="text-xs text-slate-400 mt-1.5 leading-relaxed max-w-md mx-auto">
|
||||
Thank you for contacting LiteCharms (PTY) Ltd. Your inquiry has been cataloged. Our South African helpdesk will reply within 4 business hours.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
id="btn-send-another-message"
|
||||
onClick={() => setContactSuccess(false)}
|
||||
className="mt-2 text-xs font-bold text-brand-500 hover:text-brand-400 underline cursor-pointer"
|
||||
>
|
||||
Send Another Message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleGeneralContactSubmit} className="space-y-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Your Full Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
id="contact-input-name"
|
||||
value={contactName}
|
||||
onChange={(e) => setContactName(e.target.value)}
|
||||
placeholder="Sipho Cele"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Your Email Address *</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
id="contact-input-email"
|
||||
value={contactEmail}
|
||||
onChange={(e) => setContactEmail(e.target.value)}
|
||||
placeholder="sipho@company.co.za"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Subject of Inquiry</label>
|
||||
<select
|
||||
id="contact-input-subject"
|
||||
value={contactSubject}
|
||||
onChange={(e) => setContactSubject(e.target.value)}
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white"
|
||||
>
|
||||
<option value="General Services Inquiry">General Services Inquiry</option>
|
||||
<option value="App UX Layout Request">App UX/UI Layout Request</option>
|
||||
<option value="Docker & Kubernetes Deployment Planning">Docker & Kubernetes Deployment Planning</option>
|
||||
<option value="Cloud Hosting (khongisa.co.za)">Cloud Hosting (khongisa.co.za)</option>
|
||||
<option value="Career & SLA Proposals">Career & SLA Proposals</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Detailed Message *</label>
|
||||
<textarea
|
||||
required
|
||||
id="contact-textarea-message"
|
||||
value={contactMessage}
|
||||
onChange={(e) => setContactMessage(e.target.value)}
|
||||
rows={5}
|
||||
placeholder="Specify your operational needs, site layout details, or timeframe..."
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white placeholder:text-slate-600 resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
id="contact-btn-submit"
|
||||
disabled={contactLoading}
|
||||
className="w-full py-3 px-6 rounded-xl font-bold bg-brand-500 hover:bg-brand-600 text-white shadow-md shadow-brand-500/15 flex items-center justify-center space-x-2 transition-colors disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{contactLoading ? (
|
||||
<span>Sending Message...</span>
|
||||
) : (
|
||||
<>
|
||||
<span>Send Message</span>
|
||||
<Send className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-slate-950/60 backdrop-blur-md text-slate-400 py-16 border-t border-slate-900" id="app-footer">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-12 gap-8 items-start border-b border-slate-900 pb-12">
|
||||
|
||||
{/* Branding Column */}
|
||||
<div className="md:col-span-5 space-y-4 text-left">
|
||||
<div className="flex items-center space-x-2">
|
||||
<BrandLogo className="h-10 w-auto" />
|
||||
</div>
|
||||
<p className="text-xs text-slate-400 max-w-sm leading-relaxed">
|
||||
Registered IT Services provider based in Midrand, South Africa. Delivering exceptional digital layouts and resilient containerized deployment environments since 2011.
|
||||
</p>
|
||||
<div className="text-[10px] font-mono bg-slate-950 px-3 py-2 rounded-lg border border-slate-900 inline-block text-slate-500">
|
||||
Registration No: 2011/104859/07 • Midrand, ZA
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Links Columns */}
|
||||
<div className="md:col-span-3 text-left space-y-3">
|
||||
<h5 className="text-xs font-mono font-bold text-slate-200 uppercase tracking-wider">
|
||||
Services Links
|
||||
</h5>
|
||||
<ul className="space-y-2 text-xs text-slate-400">
|
||||
<li>
|
||||
<button onClick={() => scrollToSection('services')} className="hover:text-brand-400 transition-colors cursor-pointer text-left">
|
||||
Web & Mobile App Design
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => scrollToSection('services')} className="hover:text-brand-400 transition-colors cursor-pointer text-left">
|
||||
Desktop Layout Customization
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => scrollToSection('services')} className="hover:text-brand-400 transition-colors cursor-pointer text-left">
|
||||
Docker Containerization
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => scrollToSection('services')} className="hover:text-brand-400 transition-colors cursor-pointer text-left">
|
||||
Kubernetes IaC & Hosting
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 text-left space-y-3">
|
||||
<h5 className="text-xs font-mono font-bold text-slate-200 uppercase tracking-wider">
|
||||
Company
|
||||
</h5>
|
||||
<ul className="space-y-2 text-xs text-slate-400">
|
||||
<li>
|
||||
<button onClick={() => scrollToSection('about')} className="hover:text-brand-400 transition-colors cursor-pointer text-left">
|
||||
Our History (Timeline)
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => scrollToSection('estimator')} className="hover:text-brand-400 transition-colors cursor-pointer text-left">
|
||||
Interactive Estimator
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button onClick={() => scrollToSection('faqs')} className="hover:text-brand-400 transition-colors cursor-pointer text-left">
|
||||
FAQ Hub
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2 text-left space-y-3">
|
||||
<h5 className="text-xs font-mono font-bold text-slate-200 uppercase tracking-wider">
|
||||
Specifications
|
||||
</h5>
|
||||
<div className="space-y-2">
|
||||
<span className="inline-block text-[10px] font-semibold bg-emerald-950/50 text-emerald-400 px-2.5 py-1 rounded border border-emerald-900/50">
|
||||
Container & Cloud Ready
|
||||
</span>
|
||||
<p className="text-[10px] leading-relaxed text-slate-500">
|
||||
Fully optimized container configurations with integrated Docker structures, designed for easy deployment to khongisa.co.za or custom Kubernetes setups.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="pt-8 flex flex-col md:flex-row justify-between items-center text-xs">
|
||||
<p className="text-slate-500 text-center md:text-left">
|
||||
© {new Date().getFullYear()} LiteCharms (PTY) Ltd. All Rights Reserved. Midrand, South Africa.
|
||||
</p>
|
||||
<p className="text-slate-600 mt-2 md:mt-0 text-center md:text-right">
|
||||
Designed with precision • Est. 2011 • Reg 2011/104859/07
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
export default function AuroraBackground() {
|
||||
return (
|
||||
<div className="fixed inset-0 overflow-hidden pointer-events-none -z-10" id="aurora-container">
|
||||
{/* Background base layer */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-brand-950 via-slate-900 to-brand-900 opacity-95" />
|
||||
|
||||
{/* Aurora glow blobs */}
|
||||
<div className="absolute inset-0 opacity-40 mix-blend-screen filter blur-[120px] md:blur-[160px]">
|
||||
{/* Teal blob */}
|
||||
<div className="absolute top-1/10 left-1/5 w-72 h-72 md:w-96 md:h-96 rounded-full bg-brand-500 opacity-60 animate-aurora-1" />
|
||||
|
||||
{/* Blue blob */}
|
||||
<div className="absolute top-1/3 right-1/4 w-80 h-80 md:w-[450px] md:h-[450px] rounded-full bg-accent-500 opacity-55 animate-aurora-2" />
|
||||
|
||||
{/* Cyan/Deep teal blob */}
|
||||
<div className="absolute bottom-1/4 left-1/3 w-72 h-72 md:w-[400px] md:h-[400px] rounded-full bg-brand-700 opacity-50 animate-aurora-3" />
|
||||
</div>
|
||||
|
||||
{/* Subtle grid pattern overlay for high-tech aesthetic */}
|
||||
<div className="absolute inset-0 bg-[linear-gradient(to_right,#00bcac05_1px,transparent_1px),linear-gradient(to_bottom,#00bcac05_1px,transparent_1px)] bg-[size:24px_24px]" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export function BrandIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 702.01 641.88"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fill="#002f43"
|
||||
strokeWidth="0px"
|
||||
d="M522.67,13.65H179.03L14.93,214.61l94.51,233.74,241.75,179.87,224.11-179.8,111.78-231.52L522.67,13.65ZM61.15,210.34L194.73,46.72h64.6l-57.41,214.25-140.77-50.63ZM350.2,555.04l-44-108.79-74.49-168.44,55.05-205.52,118.29,158.96,31.41,55.36-86.26,268.43ZM548.74,427.24l-165.35,132.69,84.23-262.03,169.43-53.43-88.31,182.77ZM662.62,201.71l.07.19-.12-.19h.05Z"
|
||||
/>
|
||||
<polygon
|
||||
fill="#3d98c6"
|
||||
strokeWidth="0px"
|
||||
points="637.05 244.47 548.74 427.24 383.39 559.94 467.62 297.9 637.05 244.47"
|
||||
/>
|
||||
<polygon
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
points="436.46 286.61 350.2 555.04 306.2 446.25 231.71 277.81 286.76 72.29 405.05 231.25 436.46 286.61"
|
||||
/>
|
||||
<polygon
|
||||
fill="#008092"
|
||||
strokeWidth="0px"
|
||||
points="259.33 46.72 201.92 260.97 61.15 210.34 194.73 46.72 259.33 46.72"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function BrandLogo(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 965.79 597.31"
|
||||
{...props}
|
||||
>
|
||||
<text
|
||||
transform="translate(49.84 470.99)"
|
||||
fontFamily="var(--font-sans), sans-serif"
|
||||
fontSize="44.72"
|
||||
fontWeight="700"
|
||||
fill="#002f43"
|
||||
>
|
||||
<tspan letterSpacing="-0.02em" x="0" y="0">T</tspan>
|
||||
<tspan letterSpacing="0.06em" x="26.92" y="0">AI</tspan>
|
||||
<tspan letterSpacing="0.05em" x="76.61" y="0">L</tspan>
|
||||
<tspan letterSpacing="0.06em" x="105.94" y="0">ORED</tspan>
|
||||
<tspan letterSpacing="0.02em" x="240.02" y="0"> </tspan>
|
||||
<tspan letterSpacing="0.06em" x="250.75" y="0">T</tspan>
|
||||
<tspan letterSpacing="0.06em" x="281.25" y="0">E</tspan>
|
||||
<tspan letterSpacing="0.06em" x="311.26" y="0">CHNO</tspan>
|
||||
<tspan letterSpacing="0.05em" x="447.12" y="0">L</tspan>
|
||||
<tspan letterSpacing="0.06em" x="476.46" y="0">OGY SO</tspan>
|
||||
<tspan letterSpacing="0.05em" x="659.72" y="0">L</tspan>
|
||||
<tspan letterSpacing="0.06em" x="689.37" y="0">UTIONS</tspan>
|
||||
</text>
|
||||
<polygon
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
points="473.96 527.09 459.68 564.9 407.12 528.05 473.96 527.09"
|
||||
/>
|
||||
<polygon
|
||||
fill="#002f43"
|
||||
strokeWidth="0px"
|
||||
points="515.54 542.38 459.98 565.11 459.68 564.9 473.92 527.19 515.54 542.38"
|
||||
/>
|
||||
<polygon
|
||||
fill="#3d98c6"
|
||||
strokeWidth="0px"
|
||||
points="555.82 525.9 515.54 542.38 473.92 527.19 473.95 527.09 555.82 525.9"
|
||||
/>
|
||||
<path
|
||||
fill="#002f43"
|
||||
strokeWidth="0px"
|
||||
d="M49.57,327.35h19.07v71.93h51.09v16.35H49.57v-88.28Z"
|
||||
/>
|
||||
<path
|
||||
fill="#002f43"
|
||||
strokeWidth="0px"
|
||||
d="M132.67,327.35h19.07v88.28h-19.07v-88.28Z"
|
||||
/>
|
||||
<path
|
||||
fill="#002f43"
|
||||
strokeWidth="0px"
|
||||
d="M193.42,343.7h-29.97v-16.35h79.02v16.35h-29.97v71.93h-19.08v-71.93Z"
|
||||
/>
|
||||
<path
|
||||
fill="#002f43"
|
||||
strokeWidth="0px"
|
||||
d="M254.04,327.35h69.07v16.35h-50v17.98h47.41v16.21h-47.41v21.39h51.91v16.35h-70.98v-88.28Z"
|
||||
/>
|
||||
<path
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
d="M363.69,371.35c0-25.07,19.35-45.37,46.59-45.37,12.13,0,23.02,4.09,30.92,11.17l-11.71,12.81c-6-5.18-12.13-6.68-18.12-6.68-16.08,0-27.79,12.53-27.79,28.07s11.45,28.47,27.11,28.47c8.17,0,15.67-3.13,21.12-9.13l11.85,13.35c-8.45,8.17-19.89,13.08-33.38,13.08-27.25,0-46.59-20.3-46.59-45.77Z"
|
||||
/>
|
||||
<path
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
d="M454.28,327.35h19.08v34.47h37.74v-34.47h19.08v88.28h-19.08v-37.46h-37.74v37.46h-19.08v-88.28Z"
|
||||
/>
|
||||
<path
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
d="M578.79,327.35h18.8l38.42,88.28h-19.34l-6-14.03h-44.82l-6,14.03h-19.48l38.42-88.28ZM604.68,386.75l-10.22-25.2c-2.72-6.68-6.13-15.53-6.27-15.8-.14.27-3.54,9.13-6.27,15.8l-10.22,25.2h32.97Z"
|
||||
/>
|
||||
<path
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
d="M646.08,327.35h37.87c26.02,0,39.64,15.26,39.64,34.2,0,11.31-5.86,22.21-16.76,28.06l18.39,26.02h-21.93l-13.62-20.71c-2.59.41-4.9.68-7.49.68h-17.03v20.03h-19.07v-88.28ZM684.5,379.25c11.17,0,18.94-8.04,18.94-17.71s-7.77-17.99-18.8-17.99h-19.48v35.69h19.35Z"
|
||||
/>
|
||||
<path
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
d="M737.35,327.35h16.48l13.9,19.62c5.18,7.22,15.94,22.89,15.94,22.89,0,0,10.9-15.53,15.94-22.75l13.9-19.75h16.48v88.28h-19.07v-55.86s-3.41,5.59-8.04,12.26l-19.21,27.38-19.35-27.52c-4.49-6.4-7.9-12.13-7.9-12.13v55.86h-19.07v-88.28ZM844.01,405.14l6.95-14.71c7.08,5.18,15.53,9.67,29.02,9.67,7.36,0,16.35-3.41,16.35-10.35s-7.9-8.58-15.67-10.49c-16.48-3.81-33.51-9.4-33.51-27.79,0-17.03,15.94-25.48,32.83-25.48,13.35,0,23.43,3.54,32.29,9.4l-6.95,14.71c-7.08-4.63-16.35-7.08-25.34-7.08-4.63,0-13.35,1.77-13.35,8.45,0,5.72,6.4,7.77,15.12,9.81,15.67,3.68,34.47,9.54,34.47,28.47s-18.53,27.38-36.24,27.38c-21.12,0-31.06-8.04-35.97-11.99Z"
|
||||
/>
|
||||
<path
|
||||
fill="#002f43"
|
||||
strokeWidth="0px"
|
||||
d="M554.06,32.2h-142.46l-68.03,83.31,39.18,96.9,100.22,74.57,92.91-74.54,46.34-95.98-68.16-84.26ZM362.73,113.74l55.38-67.83h26.78l-23.8,88.82-58.36-20.99ZM482.56,256.64l-18.24-45.1-30.88-69.83,22.82-85.2,49.04,65.9,13.02,22.95-35.76,111.28ZM564.87,203.66l-68.55,55.01,34.92-108.63,70.24-22.15-36.61,75.77ZM612.08,110.16l.03.08-.05-.08h.02Z"
|
||||
/>
|
||||
<polygon
|
||||
fill="#3d98c6"
|
||||
strokeWidth="0px"
|
||||
points="601.48 127.89 564.87 203.66 496.32 258.67 531.24 150.04 601.48 127.89"
|
||||
/>
|
||||
<polygon
|
||||
fill="#00bcac"
|
||||
strokeWidth="0px"
|
||||
points="518.32 145.36 482.56 256.64 464.32 211.54 433.44 141.71 456.26 56.51 505.3 122.41 518.32 145.36"
|
||||
/>
|
||||
<polygon
|
||||
fill="#008092"
|
||||
strokeWidth="0px"
|
||||
points="444.89 45.91 421.09 134.73 362.73 113.74 418.11 45.91 444.89 45.91"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { TIMELINE } from '../data';
|
||||
import { Calendar, Award, Milestone } from 'lucide-react';
|
||||
|
||||
export default function CompanyTimeline() {
|
||||
return (
|
||||
<div className="relative border-l-2 border-brand-100 ml-4 md:ml-8 pl-6 md:pl-10 space-y-12 py-4" id="company-timeline">
|
||||
{TIMELINE.map((event, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
id={`timeline-event-${event.year}`}
|
||||
className="relative group transition-all duration-300 hover:translate-x-1"
|
||||
>
|
||||
{/* Custom Node Symbol */}
|
||||
<div className="absolute -left-[35px] md:-left-[51px] top-1.5 flex items-center justify-center">
|
||||
<div className={`w-8 h-8 rounded-full border-4 flex items-center justify-center transition-all ${
|
||||
event.milestone
|
||||
? 'bg-brand-500 border-white text-white shadow-md shadow-brand-500/25 group-hover:bg-brand-600'
|
||||
: 'bg-white border-brand-200 text-brand-500 group-hover:border-brand-300'
|
||||
}`}>
|
||||
{event.milestone ? (
|
||||
<Award className="w-3.5 h-3.5" />
|
||||
) : (
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Event Card Content */}
|
||||
<div className="bg-slate-900/50 backdrop-blur-sm rounded-2xl border border-slate-800/80 p-6 shadow-sm hover:border-slate-700/80 transition-all">
|
||||
<div className="flex flex-wrap items-baseline gap-2 mb-2">
|
||||
<span className="font-display text-lg font-extrabold text-brand-500 tracking-tight">
|
||||
{event.year}
|
||||
</span>
|
||||
<span className="text-slate-600 font-mono text-sm hidden sm:inline">|</span>
|
||||
<h4 className="font-display text-sm font-bold text-white">
|
||||
{event.title}
|
||||
</h4>
|
||||
{event.milestone && (
|
||||
<span className="inline-flex items-center space-x-1 text-[9px] font-mono font-bold uppercase tracking-wider bg-brand-950/80 text-brand-400 px-2 py-0.5 rounded-md border border-brand-800/50 shrink-0">
|
||||
<Milestone className="w-2.5 h-2.5" />
|
||||
<span>Key Milestone</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs md:text-sm text-slate-300 leading-relaxed">
|
||||
{event.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useState } from 'react';
|
||||
import { FAQS } from '../data';
|
||||
import { ChevronDown, HelpCircle } from 'lucide-react';
|
||||
|
||||
export default function FAQSection() {
|
||||
const [openId, setOpenId] = useState<string | null>(null);
|
||||
|
||||
const toggleFAQ = (id: string) => {
|
||||
setOpenId(openId === id ? null : id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto space-y-4" id="faqs-accordion">
|
||||
{FAQS.map((faq) => {
|
||||
const isOpen = openId === faq.id;
|
||||
return (
|
||||
<div
|
||||
key={faq.id}
|
||||
id={`faq-item-${faq.id}`}
|
||||
className={`bg-slate-900/40 backdrop-blur-sm rounded-2xl border transition-all duration-200 ${
|
||||
isOpen
|
||||
? 'border-brand-500 shadow-md shadow-brand-500/5'
|
||||
: 'border-slate-800/80 hover:border-slate-700 hover:bg-slate-900/60'
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
id={`faq-toggle-${faq.id}`}
|
||||
onClick={() => toggleFAQ(faq.id)}
|
||||
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'}`} />
|
||||
<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' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={`transition-all duration-300 overflow-hidden ${
|
||||
isOpen ? 'max-h-56 opacity-100' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="p-6 pt-0 border-t border-slate-850 text-xs md:text-sm leading-relaxed text-slate-300">
|
||||
{faq.answer}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Menu, X, ArrowRight } from 'lucide-react';
|
||||
import { BrandIcon } from './BrandAssets';
|
||||
|
||||
interface HeaderProps {
|
||||
onScrollTo: (elementId: string) => void;
|
||||
}
|
||||
|
||||
export default function Header({ onScrollTo }: HeaderProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
if (window.scrollY > 20) {
|
||||
setIsScrolled(true);
|
||||
} else {
|
||||
setIsScrolled(false);
|
||||
}
|
||||
};
|
||||
window.addEventListener('scroll', handleScroll);
|
||||
return () => window.removeEventListener('scroll', handleScroll);
|
||||
}, []);
|
||||
|
||||
const navItems = [
|
||||
{ label: 'About', id: 'about' },
|
||||
{ label: 'Services', id: 'services' },
|
||||
{ label: 'Project Estimator', id: 'estimator' },
|
||||
{ label: 'FAQs', id: 'faqs' }
|
||||
];
|
||||
|
||||
const handleNavClick = (id: string) => {
|
||||
setIsOpen(false);
|
||||
onScrollTo(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<header
|
||||
id="app-header"
|
||||
className={`fixed top-0 left-0 w-full z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'bg-slate-950/85 backdrop-blur-md py-3 shadow-lg border-b border-slate-900/80'
|
||||
: 'bg-transparent py-5'
|
||||
}`}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
|
||||
{/* Brand Logo / Alternative Icon with LiteCharms Text */}
|
||||
<button
|
||||
id="logo-home-button"
|
||||
onClick={() => handleNavClick('hero')}
|
||||
className="flex items-center space-x-2.5 text-left group cursor-pointer"
|
||||
>
|
||||
<BrandIcon className="h-8 md:h-10 w-auto transition-transform duration-300 group-hover:scale-105" />
|
||||
<span className="font-display font-extrabold text-lg md:text-xl text-white tracking-tight group-hover:text-brand-400 transition-colors">
|
||||
Lite<span className="text-brand-400">Charms</span>
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{/* Desktop Nav */}
|
||||
<nav id="desktop-nav" className="hidden md:flex items-center space-x-8">
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
id={`nav-link-${item.id}`}
|
||||
onClick={() => handleNavClick(item.id)}
|
||||
className="text-sm font-medium text-slate-300 hover:text-brand-400 transition-colors cursor-pointer"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Header CTA */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<button
|
||||
id="header-cta-button"
|
||||
onClick={() => handleNavClick('estimator')}
|
||||
className="flex items-center space-x-1.5 px-4 py-2 text-sm font-semibold text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-all hover:shadow-md hover:shadow-brand-500/10 cursor-pointer"
|
||||
>
|
||||
<span>Interactive Estimator</span>
|
||||
<ArrowRight className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Toggle */}
|
||||
<button
|
||||
id="mobile-menu-toggle"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="md:hidden p-2 text-slate-300 hover:text-brand-400 transition-colors"
|
||||
aria-label="Toggle Menu"
|
||||
>
|
||||
{isOpen ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Panel */}
|
||||
{isOpen && (
|
||||
<div
|
||||
id="mobile-navigation-overlay"
|
||||
className="md:hidden absolute top-full left-0 w-full bg-slate-950/95 backdrop-blur-md border-b border-slate-900 shadow-xl py-6 px-6 space-y-4 flex flex-col transition-all duration-300"
|
||||
>
|
||||
{navItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
id={`mobile-nav-link-${item.id}`}
|
||||
onClick={() => handleNavClick(item.id)}
|
||||
className="text-left py-2 text-base font-semibold text-slate-300 hover:text-brand-400 transition-colors border-b border-slate-900 cursor-pointer"
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="pt-2">
|
||||
<button
|
||||
id="mobile-cta-button"
|
||||
onClick={() => handleNavClick('estimator')}
|
||||
className="w-full flex items-center justify-center space-x-2 py-3 px-4 text-center font-semibold text-white bg-brand-500 hover:bg-brand-600 rounded-lg transition-colors cursor-pointer"
|
||||
>
|
||||
<span>Launch Project Estimator</span>
|
||||
<ArrowRight className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,636 @@
|
||||
import { useState, useEffect, FormEvent } from 'react';
|
||||
import { Inquiry } from '../types';
|
||||
import { Calculator, Sparkles, Send, ShieldCheck, CheckCircle2, History, Trash2 } from 'lucide-react';
|
||||
|
||||
interface ProjectEstimatorProps {
|
||||
onInquirySubmitted: (inquiry: Inquiry) => void;
|
||||
}
|
||||
|
||||
export default function ProjectEstimator({ onInquirySubmitted }: ProjectEstimatorProps) {
|
||||
// Wizard steps state
|
||||
const [step, setStep] = useState<1 | 2>(1);
|
||||
|
||||
// Selection States
|
||||
const [serviceType, setServiceType] = useState<string>('web-design');
|
||||
const [scale, setScale] = useState<'small' | 'medium' | 'large'>('medium');
|
||||
const [options, setOptions] = useState({
|
||||
security: false,
|
||||
redundancy: false,
|
||||
legacy: false,
|
||||
support: false,
|
||||
});
|
||||
const [timeline, setTimeline] = useState<'express' | 'balanced' | 'phased'>('balanced');
|
||||
|
||||
// Contact Form States
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [phone, setPhone] = useState('');
|
||||
const [company, setCompany] = useState('');
|
||||
const [notes, setNotes] = useState('');
|
||||
const [submittedInquiries, setSubmittedInquiries] = useState<Inquiry[]>([]);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
// Load local inquiries for management
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem('litecharms_estimates');
|
||||
if (saved) {
|
||||
try {
|
||||
setSubmittedInquiries(JSON.parse(saved));
|
||||
} catch (err) {
|
||||
console.error('Error loading past estimates:', err);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Compute estimate values
|
||||
const calculateEstimate = () => {
|
||||
// Base prices (Min, Max) in ZAR
|
||||
let baseMin = 60000;
|
||||
let baseMax = 120000;
|
||||
let baseWeeks = 8;
|
||||
let serviceLabel = 'Web Application Design';
|
||||
|
||||
switch (serviceType) {
|
||||
case 'web-design':
|
||||
baseMin = 60000;
|
||||
baseMax = 120000;
|
||||
baseWeeks = 8;
|
||||
serviceLabel = 'Web Application Design';
|
||||
break;
|
||||
case 'mobile-design':
|
||||
baseMin = 80000;
|
||||
baseMax = 160000;
|
||||
baseWeeks = 10;
|
||||
serviceLabel = 'Mobile App Engineering';
|
||||
break;
|
||||
case 'desktop-design':
|
||||
baseMin = 70000;
|
||||
baseMax = 140000;
|
||||
baseWeeks = 9;
|
||||
serviceLabel = 'Desktop Software Layout';
|
||||
break;
|
||||
case 'infra-planning':
|
||||
baseMin = 25000;
|
||||
baseMax = 55000;
|
||||
baseWeeks = 4;
|
||||
serviceLabel = 'Docker Containerization & Setup';
|
||||
break;
|
||||
case 'infra-design':
|
||||
baseMin = 40000;
|
||||
baseMax = 90000;
|
||||
baseWeeks = 6;
|
||||
serviceLabel = 'Kubernetes & IaC Design';
|
||||
break;
|
||||
case 'infra-rollout':
|
||||
baseMin = 50000;
|
||||
baseMax = 110000;
|
||||
baseWeeks = 7;
|
||||
serviceLabel = 'Khongisa Private Cloud Hosting';
|
||||
break;
|
||||
}
|
||||
|
||||
// Scale multiplier
|
||||
let scaleMultiplier = 1.0;
|
||||
let scaleLabel = 'Medium Enterprise / Multi-site';
|
||||
if (scale === 'small') {
|
||||
scaleMultiplier = 0.75;
|
||||
scaleLabel = 'Small Business / Single Site';
|
||||
} else if (scale === 'large') {
|
||||
scaleMultiplier = 2.1;
|
||||
scaleLabel = 'Large Corporate / Warehouse Scale';
|
||||
}
|
||||
|
||||
let minCost = baseMin * scaleMultiplier;
|
||||
let maxCost = baseMax * scaleMultiplier;
|
||||
let estimatedWeeks = Math.round(baseWeeks * (scale === 'small' ? 0.8 : scale === 'large' ? 1.5 : 1.0));
|
||||
|
||||
const breakdown: Array<{ item: string; price: string }> = [
|
||||
{ item: `${serviceLabel} (Core Setup)`, price: `R ${(baseMin * scaleMultiplier).toLocaleString('en-ZA')} - R ${(baseMax * scaleMultiplier).toLocaleString('en-ZA')}` }
|
||||
];
|
||||
|
||||
// Add Options costs
|
||||
if (options.security) {
|
||||
const minAdd = 20000;
|
||||
const maxAdd = 40000;
|
||||
minCost += minAdd;
|
||||
maxCost += maxAdd;
|
||||
breakdown.push({ item: 'Enhanced Cryptographic & Compliance Suite', price: `R ${minAdd.toLocaleString('en-ZA')} - R ${maxAdd.toLocaleString('en-ZA')}` });
|
||||
}
|
||||
if (options.redundancy) {
|
||||
const minAdd = 15000;
|
||||
const maxAdd = 30000;
|
||||
minCost += minAdd;
|
||||
maxCost += maxAdd;
|
||||
breakdown.push({ item: 'Cloud Backups & Offsite Failovers', price: `R ${minAdd.toLocaleString('en-ZA')} - R ${maxAdd.toLocaleString('en-ZA')}` });
|
||||
}
|
||||
if (options.legacy) {
|
||||
const minAdd = 25000;
|
||||
const maxAdd = 50000;
|
||||
minCost += minAdd;
|
||||
maxCost += maxAdd;
|
||||
breakdown.push({ item: 'Legacy Software & Hardware Integrations', price: `R ${minAdd.toLocaleString('en-ZA')} - R ${maxAdd.toLocaleString('en-ZA')}` });
|
||||
}
|
||||
if (options.support) {
|
||||
const minAdd = 12000;
|
||||
const maxAdd = 24000;
|
||||
minCost += minAdd;
|
||||
maxCost += maxAdd;
|
||||
breakdown.push({ item: 'Proactive 12-Month Support SLA (Est.)', price: `R ${minAdd.toLocaleString('en-ZA')} - R ${maxAdd.toLocaleString('en-ZA')}` });
|
||||
}
|
||||
|
||||
// Timeline factors
|
||||
if (timeline === 'express') {
|
||||
minCost *= 1.3;
|
||||
maxCost *= 1.3;
|
||||
estimatedWeeks = Math.max(2, Math.round(estimatedWeeks * 0.6));
|
||||
breakdown.push({ item: 'Express Resource Acceleration Charge (+30%)', price: 'Included' });
|
||||
} else if (timeline === 'phased') {
|
||||
minCost *= 0.95;
|
||||
maxCost *= 0.95;
|
||||
estimatedWeeks = Math.round(estimatedWeeks * 1.4);
|
||||
}
|
||||
|
||||
return {
|
||||
totalMin: Math.round(minCost),
|
||||
totalMax: Math.round(maxCost),
|
||||
timelineWeeks: estimatedWeeks,
|
||||
breakdown,
|
||||
serviceLabel,
|
||||
scaleLabel,
|
||||
};
|
||||
};
|
||||
|
||||
const estimate = calculateEstimate();
|
||||
|
||||
// Submit Inquiry Proposal
|
||||
const handleInquirySubmit = async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!name || !email) return;
|
||||
|
||||
setSubmitting(true);
|
||||
|
||||
const newInquiry: Inquiry = {
|
||||
id: `est-${Date.now()}`,
|
||||
name,
|
||||
email,
|
||||
phone,
|
||||
company,
|
||||
projectType: estimate.serviceLabel,
|
||||
description: notes || `Interactive estimate query for ${estimate.serviceLabel} (${estimate.scaleLabel}).`,
|
||||
date: new Date().toLocaleDateString('en-ZA', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }),
|
||||
isEstimate: true,
|
||||
estimateSummary: {
|
||||
totalMin: estimate.totalMin,
|
||||
totalMax: estimate.totalMax,
|
||||
timelineWeeks: estimate.timelineWeeks,
|
||||
breakdown: estimate.breakdown,
|
||||
}
|
||||
};
|
||||
|
||||
// Save to state and local storage
|
||||
const updated = [newInquiry, ...submittedInquiries];
|
||||
setSubmittedInquiries(updated);
|
||||
localStorage.setItem('litecharms_estimates', JSON.stringify(updated));
|
||||
|
||||
// Propagate up
|
||||
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,
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('SMTP Proposal send failed. Saved locally:', err);
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
setSuccess(true);
|
||||
setStep(1);
|
||||
// Reset form fields
|
||||
setName('');
|
||||
setEmail('');
|
||||
setPhone('');
|
||||
setCompany('');
|
||||
setNotes('');
|
||||
}
|
||||
};
|
||||
|
||||
// Delete past estimate
|
||||
const deleteEstimate = (id: string) => {
|
||||
const updated = submittedInquiries.filter((x) => x.id !== id);
|
||||
setSubmittedInquiries(updated);
|
||||
localStorage.setItem('litecharms_estimates', JSON.stringify(updated));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12" id="estimator-panel">
|
||||
{/* Left Column: Interactive Settings Builder */}
|
||||
<div className="lg:col-span-7 space-y-8">
|
||||
{success && (
|
||||
<div className="p-6 rounded-2xl bg-emerald-950/40 border border-emerald-900/50 text-emerald-300 flex items-start space-x-4 animate-fade-in" id="estimate-success-alert">
|
||||
<CheckCircle2 className="w-6 h-6 text-emerald-500 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-display font-bold text-base text-emerald-100">Inquiry & Estimate Saved!</h4>
|
||||
<p className="text-sm mt-1">
|
||||
Your custom infrastructure and app layout estimate has been cached. Our Midrand engineering team will review your proposal and get in touch via email shortly.
|
||||
</p>
|
||||
<button
|
||||
id="btn-dismiss-success"
|
||||
onClick={() => setSuccess(false)}
|
||||
className="mt-3 text-xs font-bold underline hover:text-emerald-100 cursor-pointer"
|
||||
>
|
||||
Estimate Another Project
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-slate-900/30 backdrop-blur-md rounded-2xl border border-slate-800/80 p-8 shadow-sm space-y-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between border-b border-slate-800 pb-5">
|
||||
<div>
|
||||
<span className="text-[10px] font-mono font-bold uppercase tracking-wider text-brand-500">
|
||||
Configure Project Specs
|
||||
</span>
|
||||
<h3 className="font-display text-xl font-bold text-white">
|
||||
{step === 1 ? 'Step 1: Define Technical Scope' : 'Step 2: Contact Information'}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="text-xs font-mono font-semibold bg-slate-950/40 text-slate-400 px-3 py-1.5 rounded-lg border border-slate-800/80">
|
||||
{step === 1 ? '1 of 2' : '2 of 2'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{step === 1 ? (
|
||||
<div className="space-y-6" id="estimator-step-1">
|
||||
{/* Option 1: Select Service */}
|
||||
<div>
|
||||
<label className="block text-xs font-mono font-bold uppercase text-slate-400 tracking-wider mb-3">
|
||||
A. Choose Base Service Category
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{[
|
||||
{ id: 'web-design', label: 'Web App Design', type: 'Software' },
|
||||
{ id: 'mobile-design', label: 'Mobile App Eng', type: 'Software' },
|
||||
{ id: 'desktop-design', label: 'Desktop Software', type: 'Software' },
|
||||
{ id: 'infra-planning', label: 'Docker Containerization', type: 'Cloud & Hosting' },
|
||||
{ id: 'infra-design', label: 'Kubernetes & IaC', type: 'Cloud & Hosting' },
|
||||
{ id: 'infra-rollout', label: 'Khongisa Private Cloud', type: 'Cloud & Hosting' },
|
||||
].map((srv) => (
|
||||
<button
|
||||
key={srv.id}
|
||||
id={`select-service-${srv.id}`}
|
||||
type="button"
|
||||
onClick={() => setServiceType(srv.id)}
|
||||
className={`p-4 rounded-xl text-left border transition-all cursor-pointer ${
|
||||
serviceType === srv.id
|
||||
? 'border-brand-500 bg-brand-950/40 text-white ring-1 ring-brand-500/30'
|
||||
: 'border-slate-850 bg-slate-900/10 text-slate-300 hover:border-slate-700 hover:bg-slate-900/40'
|
||||
}`}
|
||||
>
|
||||
<span className="text-[10px] font-mono font-medium block uppercase text-slate-500 mb-1">
|
||||
{srv.type}
|
||||
</span>
|
||||
<span className="text-sm font-bold block">{srv.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option 2: Project Scale */}
|
||||
<div className="pt-4 border-t border-slate-800/80">
|
||||
<label className="block text-xs font-mono font-bold uppercase text-slate-400 tracking-wider mb-3">
|
||||
B. Select Project Scale
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ id: 'small', label: 'Small Business', desc: 'Single office / SME workflow' },
|
||||
{ id: 'medium', label: 'Medium Ent', desc: 'Standard / Multi-site hubs' },
|
||||
{ id: 'large', label: 'Large Corporate', desc: 'Enterprise / Redundant depots' },
|
||||
].map((scl) => (
|
||||
<button
|
||||
key={scl.id}
|
||||
id={`select-scale-${scl.id}`}
|
||||
type="button"
|
||||
onClick={() => setScale(scl.id as 'small' | 'medium' | 'large')}
|
||||
className={`p-4 rounded-xl text-left border transition-all cursor-pointer flex flex-col justify-between ${
|
||||
scale === scl.id
|
||||
? 'border-brand-500 bg-brand-950/40 text-white ring-1 ring-brand-500/30'
|
||||
: 'border-slate-850 bg-slate-900/10 text-slate-300 hover:border-slate-700 hover:bg-slate-900/40'
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm font-bold block">{scl.label}</span>
|
||||
<span className="text-[10px] text-slate-400 leading-tight mt-1 hidden sm:block">
|
||||
{scl.desc}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option 3: Technical Add-ons */}
|
||||
<div className="pt-4 border-t border-slate-800/80">
|
||||
<label className="block text-xs font-mono font-bold uppercase text-slate-400 tracking-wider mb-3">
|
||||
C. Technical Options & Compliance
|
||||
</label>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ id: 'security', label: 'Enhanced Security, POPIA compliance, and Intrusion audits', desc: 'Required for South African financial, logistics or medical database security.' },
|
||||
{ id: 'redundancy', label: 'Offsite Cloud Backup & Automated Failover setup', desc: 'Protects logistics operations and microservices from cloud provider hiccups or automated server cluster failovers.' },
|
||||
{ id: 'legacy', label: 'Legacy VDI System integrations or legacy database rewrites', desc: 'Ensures flawless integration with older local system mainframes, APIs, and data structures.' },
|
||||
{ id: 'support', label: 'Proactive Support Agreement (12-Month Deployment SLA)', desc: 'Provides active server uptime alarms, cloud health checks, and cluster-level updates.' },
|
||||
].map((opt) => (
|
||||
<label
|
||||
key={opt.id}
|
||||
id={`label-option-${opt.id}`}
|
||||
className={`flex items-start space-x-3 p-3 rounded-xl border transition-all cursor-pointer ${
|
||||
(options as any)[opt.id]
|
||||
? 'border-brand-500 bg-brand-950/20'
|
||||
: 'border-slate-850 hover:border-slate-700 hover:bg-slate-900/10'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id={`checkbox-option-${opt.id}`}
|
||||
checked={(options as any)[opt.id]}
|
||||
onChange={(e) => setOptions({ ...options, [opt.id]: e.target.checked })}
|
||||
className="mt-1 accent-brand-500 rounded"
|
||||
/>
|
||||
<div>
|
||||
<span className="text-xs font-bold text-white block">{opt.label}</span>
|
||||
<span className="text-[10px] text-slate-400 leading-relaxed block mt-0.5">{opt.desc}</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Option 4: Timeline Priority */}
|
||||
<div className="pt-4 border-t border-slate-800/80">
|
||||
<label className="block text-xs font-mono font-bold uppercase text-slate-400 tracking-wider mb-3">
|
||||
D. Timeline Urgency
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{[
|
||||
{ id: 'express', label: 'Express Delivery', desc: 'Fast-track (2-4 weeks)' },
|
||||
{ id: 'balanced', label: 'Balanced Schedule', desc: 'Standard (6-12 weeks)' },
|
||||
{ id: 'phased', label: 'Phased Rollout', desc: 'Gradual (3-6 months)' },
|
||||
].map((time) => (
|
||||
<button
|
||||
key={time.id}
|
||||
id={`select-timeline-${time.id}`}
|
||||
type="button"
|
||||
onClick={() => setTimeline(time.id as 'express' | 'balanced' | 'phased')}
|
||||
className={`p-3.5 rounded-xl text-center border transition-all cursor-pointer ${
|
||||
timeline === time.id
|
||||
? 'border-brand-500 bg-brand-950/40 text-white ring-1 ring-brand-500/30'
|
||||
: 'border-slate-850 bg-slate-900/10 text-slate-300 hover:border-slate-700 hover:bg-slate-900/40'
|
||||
}`}
|
||||
>
|
||||
<span className="text-xs font-bold block">{time.label}</span>
|
||||
<span className="text-[9px] text-slate-400 block mt-1">{time.desc}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Proceed CTA */}
|
||||
<div className="pt-4">
|
||||
<button
|
||||
type="button"
|
||||
id="btn-goto-step-2"
|
||||
onClick={() => setStep(2)}
|
||||
className="w-full py-3.5 px-6 rounded-xl font-bold bg-brand-500 hover:bg-brand-600 text-white shadow-md shadow-brand-500/10 flex items-center justify-center space-x-2 transition-colors cursor-pointer"
|
||||
>
|
||||
<span>Lock in Specs & Submit Details</span>
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleInquirySubmit} className="space-y-5 animate-fade-in" id="estimator-step-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Your Full Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
id="input-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="e.g. Sipho Cele"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Your Email Address *</label>
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
id="input-email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="sipho@company.co.za"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Phone Number</label>
|
||||
<input
|
||||
type="tel"
|
||||
id="input-phone"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
placeholder="e.g. +27 11 123 4567"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Company / Organization</label>
|
||||
<input
|
||||
type="text"
|
||||
id="input-company"
|
||||
value={company}
|
||||
onChange={(e) => setCompany(e.target.value)}
|
||||
placeholder="e.g. Midrand Retail Group"
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm bg-slate-900/50 text-white placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-slate-400 mb-1">Additional Project Brief / Special Requests</label>
|
||||
<textarea
|
||||
id="textarea-notes"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={4}
|
||||
placeholder="Describe your container requirements, current deployment challenges, or domain details in Gauteng..."
|
||||
className="w-full px-4 py-2.5 rounded-xl border border-slate-800 focus:outline-none focus:ring-2 focus:ring-brand-500 text-sm resize-none bg-slate-900/50 text-white placeholder:text-slate-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-3 pt-2">
|
||||
<button
|
||||
type="button"
|
||||
id="btn-back-to-step-1"
|
||||
onClick={() => setStep(1)}
|
||||
className="w-1/3 py-3 px-4 rounded-xl border border-slate-800 hover:bg-slate-900 font-semibold text-slate-300 text-sm transition-colors cursor-pointer"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
id="btn-submit-proposal"
|
||||
disabled={submitting}
|
||||
className="w-2/3 py-3 px-4 rounded-xl font-bold bg-brand-500 hover:bg-brand-600 text-white shadow-md shadow-brand-500/10 flex items-center justify-center space-x-2 transition-colors disabled:opacity-50 cursor-pointer"
|
||||
>
|
||||
{submitting ? (
|
||||
<span>Processing Setup...</span>
|
||||
) : (
|
||||
<>
|
||||
<span>Submit Estimate Proposal</span>
|
||||
<Send className="w-4 h-4" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Visual Summary Proposal card */}
|
||||
<div className="lg:col-span-5 space-y-6">
|
||||
<div className="bg-slate-900/50 backdrop-blur-md text-slate-100 rounded-2xl p-8 border border-slate-800/80 shadow-xl sticky top-28 space-y-6">
|
||||
<div className="flex items-center space-x-2 border-b border-slate-800 pb-5">
|
||||
<Calculator className="w-5 h-5 text-brand-500" />
|
||||
<h4 className="font-display font-bold text-lg text-white">Instant Proposal Draft</h4>
|
||||
</div>
|
||||
|
||||
{/* Pricing Range */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-[10px] font-mono font-medium text-slate-400 uppercase tracking-widest">
|
||||
Estimated Investment Range (ZAR)
|
||||
</p>
|
||||
<div className="flex items-baseline space-x-2">
|
||||
<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="font-display text-2xl md:text-3xl font-extrabold text-accent-400">
|
||||
R {estimate.totalMax.toLocaleString('en-ZA')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-400 leading-relaxed pt-1">
|
||||
*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">
|
||||
Completion Roadmap
|
||||
</p>
|
||||
<p className="font-bold text-sm text-white mt-1">
|
||||
~ {estimate.timelineWeeks} Weeks
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[9px] font-mono text-slate-500 uppercase tracking-wider">
|
||||
Priority Mode
|
||||
</p>
|
||||
<p className="font-bold text-sm text-brand-500 mt-1 capitalize">
|
||||
{timeline} Schedule
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breakdown Items */}
|
||||
<div className="space-y-3">
|
||||
<p className="text-[10px] font-mono font-bold uppercase text-slate-400 tracking-wider">
|
||||
Selected Item Breakdown:
|
||||
</p>
|
||||
<div className="space-y-2 text-xs">
|
||||
{estimate.breakdown.map((item, idx) => (
|
||||
<div key={idx} className="flex justify-between items-start space-x-4">
|
||||
<span className="text-slate-300 leading-normal">{item.item}</span>
|
||||
<span className="font-mono text-slate-400 font-semibold shrink-0">{item.price}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Compliance Box */}
|
||||
<div className="p-4 rounded-xl bg-slate-950/80 border border-slate-800 flex items-start space-x-3 text-xs text-slate-400">
|
||||
<ShieldCheck className="w-5 h-5 text-brand-500 shrink-0 mt-0.5" />
|
||||
<p className="leading-relaxed">
|
||||
Designed by <strong className="text-brand-500">LiteCharms (PTY) Ltd</strong> under compliance of South African cloud safety and secure container isolation standards.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Saved Estimates Log (Local Memory Management) */}
|
||||
{submittedInquiries.length > 0 && (
|
||||
<div className="bg-slate-900/30 backdrop-blur-md rounded-2xl border border-slate-800/80 p-6 shadow-sm space-y-4" id="past-estimates-container">
|
||||
<div className="flex items-center space-x-2 text-white border-b border-slate-850 pb-3">
|
||||
<History className="w-4 h-4 text-brand-500" />
|
||||
<h4 className="font-display font-bold text-sm">Your Estimates Log ({submittedInquiries.length})</h4>
|
||||
</div>
|
||||
<div className="space-y-3 max-h-56 overflow-y-auto pr-1">
|
||||
{submittedInquiries.map((inq) => (
|
||||
<div
|
||||
key={inq.id}
|
||||
id={`saved-estimate-${inq.id}`}
|
||||
className="p-3 bg-slate-900/40 border border-slate-850 rounded-xl relative hover:border-slate-700 transition-all text-xs"
|
||||
>
|
||||
<button
|
||||
id={`btn-delete-estimate-${inq.id}`}
|
||||
type="button"
|
||||
onClick={() => deleteEstimate(inq.id)}
|
||||
className="absolute top-3 right-3 text-slate-500 hover:text-red-400 transition-colors cursor-pointer"
|
||||
title="Delete Draft"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<div className="pr-6 space-y-1">
|
||||
<p className="font-bold text-white">{inq.projectType}</p>
|
||||
<p className="text-[10px] text-slate-500 font-mono">Submitted: {inq.date}</p>
|
||||
{inq.estimateSummary && (
|
||||
<div className="flex items-center space-x-1 font-mono text-[10px] font-bold text-brand-400 mt-1">
|
||||
<span>Range:</span>
|
||||
<span>
|
||||
R {inq.estimateSummary.totalMin.toLocaleString('en-ZA')} - R {inq.estimateSummary.totalMax.toLocaleString('en-ZA')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import { useState } from 'react';
|
||||
import { Service } from '../types';
|
||||
import { SERVICES } from '../data';
|
||||
import * as Icons from 'lucide-react';
|
||||
|
||||
export default function ServiceExplorer() {
|
||||
const [activeTab, setActiveTab] = useState<'all' | 'apps' | 'infra'>('all');
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null);
|
||||
|
||||
// Dynamic icon helper
|
||||
const renderIcon = (iconName: string) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const IconComp = (Icons as any)[iconName];
|
||||
if (!IconComp) return <Icons.HelpCircle className="w-6 h-6" />;
|
||||
return <IconComp className="w-6 h-6" />;
|
||||
};
|
||||
|
||||
const filteredServices = SERVICES.filter((service) => {
|
||||
if (activeTab === 'all') return true;
|
||||
return service.category === activeTab;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-12">
|
||||
{/* Category Tabs */}
|
||||
<div className="flex justify-center" id="service-tabs-container">
|
||||
<div className="inline-flex bg-slate-950/40 p-1.5 rounded-xl border border-slate-800/80">
|
||||
<button
|
||||
id="tab-all"
|
||||
onClick={() => {
|
||||
setActiveTab('all');
|
||||
setExpandedId(null);
|
||||
}}
|
||||
className={`px-6 py-2.5 rounded-lg text-sm font-semibold transition-all duration-200 cursor-pointer ${
|
||||
activeTab === 'all'
|
||||
? 'bg-brand-500 text-white shadow-md'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/40'
|
||||
}`}
|
||||
>
|
||||
All Services
|
||||
</button>
|
||||
<button
|
||||
id="tab-apps"
|
||||
onClick={() => {
|
||||
setActiveTab('apps');
|
||||
setExpandedId(null);
|
||||
}}
|
||||
className={`px-6 py-2.5 rounded-lg text-sm font-semibold transition-all duration-200 cursor-pointer ${
|
||||
activeTab === 'apps'
|
||||
? 'bg-brand-500 text-white shadow-md'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/40'
|
||||
}`}
|
||||
>
|
||||
App Design
|
||||
</button>
|
||||
<button
|
||||
id="tab-infra"
|
||||
onClick={() => {
|
||||
setActiveTab('infra');
|
||||
setExpandedId(null);
|
||||
}}
|
||||
className={`px-6 py-2.5 rounded-lg text-sm font-semibold transition-all duration-200 cursor-pointer ${
|
||||
activeTab === 'infra'
|
||||
? 'bg-brand-500 text-white shadow-md'
|
||||
: 'text-slate-400 hover:text-slate-200 hover:bg-slate-900/40'
|
||||
}`}
|
||||
>
|
||||
Cloud & Hosting
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8" id="services-grid">
|
||||
{filteredServices.map((service) => {
|
||||
const isExpanded = expandedId === service.id;
|
||||
return (
|
||||
<div
|
||||
key={service.id}
|
||||
id={`service-card-${service.id}`}
|
||||
className={`bg-slate-900/40 backdrop-blur-sm rounded-2xl border transition-all duration-300 relative overflow-hidden flex flex-col justify-between ${
|
||||
isExpanded
|
||||
? 'border-brand-500 shadow-lg shadow-brand-500/5 ring-1 ring-brand-500/50'
|
||||
: 'border-slate-800/80 shadow-sm hover:border-slate-700 hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<div className="p-8">
|
||||
{/* Header Icon + Label */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className={`w-12 h-12 rounded-xl flex items-center justify-center transition-colors ${
|
||||
isExpanded ? 'bg-brand-500 text-white' : 'bg-slate-900/50 text-brand-500 border border-slate-800'
|
||||
}`}>
|
||||
{renderIcon(service.icon)}
|
||||
</div>
|
||||
<span className={`text-xs font-mono font-semibold uppercase tracking-wider px-2.5 py-1 rounded-full ${
|
||||
service.category === 'apps'
|
||||
? 'bg-sky-950/50 text-sky-400 border border-sky-900/50'
|
||||
: 'bg-emerald-950/50 text-emerald-400 border border-emerald-900/50'
|
||||
}`}>
|
||||
{service.category === 'apps' ? 'Software Design' : 'Cloud & Hosting'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Title & Desc */}
|
||||
<h3 className="font-display text-xl font-bold text-white mb-3">
|
||||
{service.title}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed text-slate-300 mb-6">
|
||||
{service.description}
|
||||
</p>
|
||||
|
||||
{/* Expanded Capabilities List */}
|
||||
<div
|
||||
className={`transition-all duration-300 overflow-hidden ${
|
||||
isExpanded ? 'max-h-80 opacity-100 mb-2' : 'max-h-0 opacity-0'
|
||||
}`}
|
||||
>
|
||||
<div className="pt-4 border-t border-slate-800/80 space-y-3">
|
||||
<p className="text-xs font-mono font-medium uppercase tracking-wider text-brand-500">
|
||||
Core Capabilities:
|
||||
</p>
|
||||
{service.details.map((detail, idx) => (
|
||||
<div key={idx} className="flex items-start space-x-2">
|
||||
<Icons.Check className="w-4 h-4 text-brand-500 shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-slate-300 leading-normal">
|
||||
{detail}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Trigger Card Footer */}
|
||||
<div className="px-8 pb-8 pt-2">
|
||||
<button
|
||||
id={`btn-toggle-capabilities-${service.id}`}
|
||||
onClick={() => setExpandedId(isExpanded ? null : service.id)}
|
||||
className={`w-full py-2.5 px-4 rounded-lg text-xs font-bold transition-all flex items-center justify-center space-x-1.5 cursor-pointer border ${
|
||||
isExpanded
|
||||
? 'bg-brand-950/80 text-brand-400 border-brand-800/50 hover:bg-brand-900'
|
||||
: 'bg-slate-900/50 text-slate-300 border-slate-800/80 hover:bg-slate-900 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<span>{isExpanded ? 'Hide Capabilities' : 'Explore Capabilities'}</span>
|
||||
<Icons.ChevronDown
|
||||
className={`w-3.5 h-3.5 transition-transform duration-300 ${
|
||||
isExpanded ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
import { Service, CaseStudy, FAQ, TeamMember, TimelineEvent } from './types';
|
||||
|
||||
export const SERVICES: Service[] = [
|
||||
{
|
||||
id: 'web-design',
|
||||
title: 'Web Application Design',
|
||||
description: 'Bespoke, high-performance web applications tailored to streamline operations, engage customers, and scale with your growth.',
|
||||
details: [
|
||||
'Interactive visual designs and dynamic prototypes',
|
||||
'Full-stack architecture layout and implementation paths',
|
||||
'Modern Single-Page (SPA) & Progressive Web App (PWA) workflows',
|
||||
'Tailwind CSS & responsive layouts for flawless desktop/mobile experience',
|
||||
'Optimized performance, SEO-friendly architecture, and analytics hooks'
|
||||
],
|
||||
icon: 'Globe',
|
||||
category: 'apps'
|
||||
},
|
||||
{
|
||||
id: 'mobile-design',
|
||||
title: 'Mobile App Engineering',
|
||||
description: 'Immersive native and cross-platform mobile solutions designed to maximize touch-point engagements and performance on Android & iOS.',
|
||||
details: [
|
||||
'Tailored iOS and Android user interface design guidelines',
|
||||
'Cross-platform React Native and Flutter design planning',
|
||||
'Sensory haptic feedback and custom touch gesture micro-interactions',
|
||||
'Offline-first synchronization patterns and secure local databases',
|
||||
'App Store & Google Play launch compliance planning'
|
||||
],
|
||||
icon: 'Smartphone',
|
||||
category: 'apps'
|
||||
},
|
||||
{
|
||||
id: 'desktop-design',
|
||||
title: 'Desktop Software Layout',
|
||||
description: 'Robust, system-level applications engineered for heavy productivity, secure offline storage, and deep hardware integration.',
|
||||
details: [
|
||||
'Windows, macOS, and Linux multi-platform layouts',
|
||||
'High-throughput workflows for administrative and medical staff',
|
||||
'Legacy software modernization and interface overhauls',
|
||||
'Direct local hardware and specialized peripheral integration',
|
||||
'Resource-optimized runtime layout for enterprise security'
|
||||
],
|
||||
icon: 'Laptop',
|
||||
category: 'apps'
|
||||
},
|
||||
{
|
||||
id: 'infra-planning',
|
||||
title: 'Docker Containerization & Setup',
|
||||
description: 'We package your applications into highly efficient, reproducible Docker containers, ensuring flawless portability and rapid dev-to-prod deployment cycles.',
|
||||
details: [
|
||||
'Custom Dockerfile creation and multi-stage build optimization',
|
||||
'Base image security scanning and vulnerability remediation',
|
||||
'Environment variable management and containerized state flows',
|
||||
'Docker Compose orchestration for multi-container local environments',
|
||||
'Standardized container handovers ready to launch on any cloud provider'
|
||||
],
|
||||
icon: 'Layers',
|
||||
category: 'infra'
|
||||
},
|
||||
{
|
||||
id: 'infra-design',
|
||||
title: 'Kubernetes & Infrastructure as Code',
|
||||
description: 'Architecting elastic, high-availability cluster designs using Kubernetes, with infrastructure fully defined and managed as declarative code (IaC).',
|
||||
details: [
|
||||
'Declarative IaC design using Terraform, OpenTofu, or Pulumi scripts',
|
||||
'Kubernetes cluster design, deployment manifests, and Helm chart packaging',
|
||||
'High-uptime load balancing, auto-scaling thresholds, and ingress routing',
|
||||
'Secure secrets storage, network policies, and cluster namespace isolation',
|
||||
'GitOps workflow planning (e.g., ArgoCD or GitHub Actions pipelines)'
|
||||
],
|
||||
icon: 'Cpu',
|
||||
category: 'infra'
|
||||
},
|
||||
{
|
||||
id: 'infra-rollout',
|
||||
title: 'Khongisa Private Cloud Hosting',
|
||||
description: 'Premium application hosting environments via our own private cloud infrastructure (khongisa.co.za), built specifically for clients without pre-existing hosting.',
|
||||
details: [
|
||||
'High-availability app hosting and server rollout on khongisa.co.za',
|
||||
'Multi-tenant isolation, automatic SSL provisioning, and domain routing',
|
||||
'Proactive load balancing and server-level hardware failure failover',
|
||||
'24/7/365 infrastructure monitoring, metrics gathering, and alarm systems',
|
||||
'Daily offsite backups, incremental rollback points, and managed databases'
|
||||
],
|
||||
icon: 'Server',
|
||||
category: 'infra'
|
||||
}
|
||||
];
|
||||
|
||||
export const CASE_STUDIES: CaseStudy[] = [
|
||||
{
|
||||
id: 'case-1',
|
||||
title: 'Gauteng Logistics Hub Cloud & IaC Migration',
|
||||
client: 'Gauteng Freight Solutions',
|
||||
year: '2023',
|
||||
category: 'Infrastructure',
|
||||
description: 'Blueprinted and migrated a multi-tenant microservices stack onto our private cloud infrastructure (khongisa.co.za) for a warehouse operation in Midrand.',
|
||||
challenge: 'The warehouse suffered from legacy local server halts, slow inventory sync times, and zero redundancy in their physical setups, leading to frequent operational bottlenecks during peak hours.',
|
||||
solution: 'Designed and implemented declarative Infrastructure as Code (IaC) blueprints, dockerized the warehouse APIs, and successfully rolled out an auto-scaling Kubernetes cluster hosted on khongisa.co.za.',
|
||||
outcome: 'Eliminated all physical downtime risk, reduced handheld scanning API latencies from 4.2 seconds to 80 milliseconds, and implemented instant automated backups.',
|
||||
image: 'https://images.unsplash.com/photo-1558494949-ef010cbdcc31?auto=format&fit=crop&w=1000&q=80',
|
||||
tags: ['Docker Setup', 'Kubernetes Clusters', 'khongisa.co.za Hosting', 'IaC Terraform']
|
||||
},
|
||||
{
|
||||
id: 'case-2',
|
||||
title: 'Retail Mobile App Design & Portal',
|
||||
client: 'LiteShop Retailers Group',
|
||||
year: '2024',
|
||||
category: 'App Design',
|
||||
description: 'Designed a comprehensive retail management suite consisting of an Android app for in-store inventory and an executive Web dashboard.',
|
||||
challenge: 'Staff were struggling with clunky, 15-year-old handheld terminal interfaces, causing long queues at cash registers and high margin error in inventory count.',
|
||||
solution: 'Engineered high-fidelity, high-contrast UI layouts for Android-based scanning devices, focusing on single-hand reachability. Created a matching React-based administrative dashboard utilizing interactive charts to display sales and stock metrics.',
|
||||
outcome: 'Reduced staff onboarding training time by 70%, boosted in-store inventory auditing speed by 45%, and received an internal user satisfaction score of 98%.',
|
||||
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?auto=format&fit=crop&w=1000&q=80',
|
||||
tags: ['UX/UI Layout', 'Mobile Wireframing', 'React Web Dashboard', 'Typography Pairing']
|
||||
},
|
||||
{
|
||||
id: 'case-3',
|
||||
title: 'Soweto Tech Incubator Docker & Hosting Rollout',
|
||||
client: 'eKasi Founders Alliance',
|
||||
year: '2022',
|
||||
category: 'Infrastructure & Apps',
|
||||
description: 'Designed and deployed the core containerized workspace services and a digital member onboarding portal for a modern tech hub.',
|
||||
challenge: 'The incubator required rapid application deployment capabilities for local startups who lacked dedicated servers, hosting, or deployment setups.',
|
||||
solution: 'We containerized the startups\' apps using custom Dockerfiles, established a secure reverse-proxy architecture, and launched their hosting environments on khongisa.co.za with individual subdomains.',
|
||||
outcome: 'Successfully established high-performance hosting for 80 concurrent developers, and processed over 10,000 workspace desk reservations on our portal.',
|
||||
image: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?auto=format&fit=crop&w=1000&q=80',
|
||||
tags: ['Docker Compose', 'App Hosting', 'khongisa.co.za', 'Subdomain Routing']
|
||||
}
|
||||
];
|
||||
|
||||
export const TIMELINE: TimelineEvent[] = [
|
||||
{
|
||||
year: '2011',
|
||||
title: 'Founding in Midrand',
|
||||
description: 'LiteCharms (PTY) Ltd was established in Midrand, South Africa, focused on custom mobile application wireframing and software consultation.',
|
||||
milestone: true
|
||||
},
|
||||
{
|
||||
year: '2014',
|
||||
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: '2017',
|
||||
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: '2020',
|
||||
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: '2023',
|
||||
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
|
||||
},
|
||||
{
|
||||
year: '2026',
|
||||
title: 'Modern App & Cloud Architectures',
|
||||
description: 'Deploying highly responsive web and mobile layouts backed by automated GitOps deployment pipelines into our robust khongisa.co.za private cloud environment.',
|
||||
milestone: false
|
||||
}
|
||||
];
|
||||
|
||||
export const TEAM: TeamMember[] = [
|
||||
{
|
||||
name: 'Thabo Khumalo',
|
||||
role: 'Managing Director & Lead Solutions Architect',
|
||||
bio: 'With over 18 years of experience in distributed cloud topologies and virtualization, Thabo oversees all container deployments and private cloud environments. He holds advanced professional cloud certifications.',
|
||||
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=400&h=400&q=80',
|
||||
education: 'BSc Computer Science (Wits), Certified Kubernetes Administrator'
|
||||
},
|
||||
{
|
||||
name: 'Sarah van der Merwe',
|
||||
role: 'Director of App Design & User Experience',
|
||||
bio: 'Sarah leads our application design squad. She specializes in crafting highly intuitive mobile, desktop, and web layouts, blending sleek aesthetics with deep ergonomics to maximize system productivity.',
|
||||
avatar: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&w=400&h=400&q=80',
|
||||
education: 'BA Information Design (UP), UX Certified Practitioner'
|
||||
},
|
||||
{
|
||||
name: 'David Nkosi',
|
||||
role: 'Cloud Operations Manager',
|
||||
bio: 'David manages our virtual deployments and container environments. From Terraform script configurations to Kubernetes ingress routing and signal auditing on khongisa.co.za, David ensures smooth deployments.',
|
||||
avatar: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=400&h=400&q=80',
|
||||
education: 'National Diploma in Electrical Engineering (TUT), Professional DevOps Engineer'
|
||||
}
|
||||
];
|
||||
|
||||
export const FAQS: FAQ[] = [
|
||||
{
|
||||
id: 'faq-1',
|
||||
question: 'Where is LiteCharms located, and do you service other regions?',
|
||||
answer: 'Our main office is situated in Midrand, South Africa, making us perfectly positioned to service the entire Gauteng region (including Johannesburg, Pretoria, and surrounding areas). For major cloud contracts, we also execute plans and virtual rollouts across other provinces and neighboring Southern African countries.'
|
||||
},
|
||||
{
|
||||
id: 'faq-2',
|
||||
question: 'What if we do not have our own server or hosting setup?',
|
||||
answer: 'We provide premium application hosting environments on our private cloud platform, khongisa.co.za. This is built specifically for clients who want highly responsive, secure, and fully managed cloud infrastructure without the hassle of configuring external accounts.'
|
||||
},
|
||||
{
|
||||
id: 'faq-3',
|
||||
question: 'Can you work with our existing in-house IT and development teams?',
|
||||
answer: 'Absolutely. We regularly operate as secondary architectural consultants. We can containerize your systems using Docker, configure your Infrastructure as Code (IaC) manifests, or design complete Kubernetes cluster architectures for your in-house teams to maintain.'
|
||||
},
|
||||
{
|
||||
id: 'faq-4',
|
||||
question: 'What is your process for cloud infrastructure planning?',
|
||||
answer: 'We follow a rigorous five-step flow: 1) Application audit & dependency mapping; 2) Portability configuration via Docker; 3) Declarative IaC blueprinting (Terraform/OpenTofu); 4) Kubernetes orchestration or khongisa.co.za deployment; 5) Active monitoring setup & SLA handover.'
|
||||
},
|
||||
{
|
||||
id: 'faq-5',
|
||||
question: 'Is your hosting environment secure and POPIA compliant?',
|
||||
answer: 'Yes, our private cloud infrastructure (khongisa.co.za) is built under strict compliance guidelines. We implement absolute multi-tenant container isolation, automated SSL encryption, firewalls, and data protection structures conforming to South African digital safety standards.'
|
||||
}
|
||||
];
|
||||
@@ -0,0 +1,80 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
--font-display: "Outfit", sans-serif;
|
||||
--font-mono: "JetBrains Mono", monospace;
|
||||
|
||||
--color-brand-50: #e6fcfb;
|
||||
--color-brand-100: #bbf7f3;
|
||||
--color-brand-200: #80eedf;
|
||||
--color-brand-500: #00bcac;
|
||||
--color-brand-600: #00a294;
|
||||
--color-brand-700: #008092;
|
||||
--color-brand-800: #005d6a;
|
||||
--color-brand-900: #002f43;
|
||||
--color-brand-950: #001d2a;
|
||||
|
||||
--color-accent-400: #5eb3de;
|
||||
--color-accent-500: #3d98c6;
|
||||
--color-accent-600: #2b7da8;
|
||||
|
||||
--animate-aurora-1: aurora-1 25s infinite alternate ease-in-out;
|
||||
--animate-aurora-2: aurora-2 30s infinite alternate ease-in-out;
|
||||
--animate-aurora-3: aurora-3 20s infinite alternate ease-in-out;
|
||||
|
||||
@keyframes aurora-1 {
|
||||
0%, 100% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: translate(40px, -60px) scale(1.15);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-30px, 30px) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aurora-2 {
|
||||
0%, 100% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50px, 50px) scale(1.2);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aurora-3 {
|
||||
0%, 100% {
|
||||
transform: translate(0px, 0px) scale(1);
|
||||
}
|
||||
40% {
|
||||
transform: translate(60px, 30px) scale(0.85);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f8fafc;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Base custom classes */
|
||||
.font-display {
|
||||
font-family: var(--font-display);
|
||||
}
|
||||
.font-mono {
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import {StrictMode} from 'react';
|
||||
import {createRoot} from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './index.css';
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,62 @@
|
||||
export interface Service {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
details: string[];
|
||||
icon: string;
|
||||
category: 'apps' | 'infra';
|
||||
}
|
||||
|
||||
export interface CaseStudy {
|
||||
id: string;
|
||||
title: string;
|
||||
client: string;
|
||||
year: string;
|
||||
category: string;
|
||||
description: string;
|
||||
challenge: string;
|
||||
solution: string;
|
||||
outcome: string;
|
||||
image: string;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
export interface Inquiry {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
company?: string;
|
||||
projectType: string;
|
||||
budgetRange?: string;
|
||||
description: string;
|
||||
date: string;
|
||||
isEstimate: boolean;
|
||||
estimateSummary?: {
|
||||
totalMin: number;
|
||||
totalMax: number;
|
||||
timelineWeeks: number;
|
||||
breakdown: Array<{ item: string; price: string }>;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FAQ {
|
||||
id: string;
|
||||
question: string;
|
||||
answer: string;
|
||||
}
|
||||
|
||||
export interface TeamMember {
|
||||
name: string;
|
||||
role: string;
|
||||
bio: string;
|
||||
avatar: string;
|
||||
education?: string;
|
||||
}
|
||||
|
||||
export interface TimelineEvent {
|
||||
year: string;
|
||||
title: string;
|
||||
description: string;
|
||||
milestone: boolean;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2022",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"allowJs": true,
|
||||
"jsx": "react-jsx",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import path from 'path';
|
||||
import {defineConfig} from 'vite';
|
||||
|
||||
export default defineConfig(() => {
|
||||
return {
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, '.'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
// HMR is disabled in AI Studio via DISABLE_HMR env var.
|
||||
// Do not modifyâfile watching is disabled to prevent flickering during agent edits.
|
||||
hmr: process.env.DISABLE_HMR !== 'true',
|
||||
// Disable file watching when DISABLE_HMR is true to save CPU during agent edits.
|
||||
watch: process.env.DISABLE_HMR === 'true' ? null : {},
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user