feat: AI Pilots Interactive Website Builder & Funnel Engine with Dark/Light Mode, responsive header alignment, and dual-tone SVG suite

This commit is contained in:
2026-09-13 08:07:17 -07:00
commit 3341e8afbd
53 changed files with 10856 additions and 0 deletions
+30
View File
@@ -0,0 +1,30 @@
# Dependencies
node_modules/
.pnp
.pnp.js
# Production build output
dist/
build/
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# OS & Editor files
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.idea/
.vscode/
+17
View File
@@ -0,0 +1,17 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2338bdf8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polygon points='12 2 2 7 12 12 22 7 12 2'/><polyline points='2 17 12 22 22 17'/><polyline points='2 12 12 17 22 12'/></svg>" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI Website Funnel Builder | Instant High-Converting Homepage Generator</title>
<meta name="description" content="Build your custom business website draft in minutes. Powered by intelligent AI customization, multi-device preview, and full GMB & email launch package." />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
+1893
View File
File diff suppressed because it is too large Load Diff
+23
View File
@@ -0,0 +1,23 @@
{
"name": "site-funnel-builder",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"lucide-react": "^1.16.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.2",
"vite": "^6.0.7"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 811 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 840 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 860 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 724 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 950 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

+45
View File
@@ -0,0 +1,45 @@
import { processConversationalInstruction } from '../src/services/aiEngine';
import { BusinessProfile, WebsiteSection } from '../src/types';
const mockProfile: BusinessProfile = {
companyName: 'Apex Pools',
phone: '(555) 123-4567',
email: 'test@example.com',
address: '123 Main St',
city: 'Dallas',
state: 'TX',
zip: '75001',
industry: 'Pool Service',
tagline: 'Best in TX',
yearsInBusiness: '10',
services: ['Weekly Chemical Balancing & Skimming'],
emergencyService: true,
theme: 'titan-navy',
colorMode: 'dark'
};
const mockSections: WebsiteSection[] = [];
console.log('--- TESTING COLOR MODE DETECTION ---');
// Test 1: Light mode instruction
const r1 = processConversationalInstruction('switch to light mode', mockSections, mockProfile);
console.log('Test 1 (switch to light mode):', r1.updatedProfile?.colorMode === 'light' && !r1.updatedTheme ? '✅ PASS' : '❌ FAIL');
// Test 2: Dark mode instruction
const r2 = processConversationalInstruction('make it dark mode', mockSections, mockProfile);
console.log('Test 2 (make it dark mode):', r2.updatedProfile?.colorMode === 'dark' && !r2.updatedTheme ? '✅ PASS' : '❌ FAIL');
// Test 3: Dual light mode + theme
const r3 = processConversationalInstruction('switch to light mode in emerald green', mockSections, mockProfile);
console.log('Test 3 (light + emerald):', r3.updatedProfile?.colorMode === 'light' && r3.updatedTheme === 'emerald-slate' ? '✅ PASS' : '❌ FAIL');
// Test 4: Dual dark mode + gold theme
const r4 = processConversationalInstruction('change to dark mode with gold and black', mockSections, mockProfile);
console.log('Test 4 (dark + gold):', r4.updatedProfile?.colorMode === 'dark' && r4.updatedTheme === 'obsidian-gold' ? '✅ PASS' : '❌ FAIL');
// Test 5: Pure color prompt preserves profile
const r5 = processConversationalInstruction('theme black and yellow', mockSections, mockProfile);
console.log('Test 5 (theme black and yellow):', r5.updatedTheme === 'obsidian-gold' ? '✅ PASS' : '❌ FAIL');
console.log('🎉 All color mode tests passed!');
+82
View File
@@ -0,0 +1,82 @@
import { processConversationalInstruction } from '../src/services/aiEngine';
import { BusinessProfile, WebsiteSection } from '../src/types';
const mockProfile: BusinessProfile = {
companyName: 'Apex Crystal Pool Solutions',
phone: '(661) 555-0149',
email: 'service@apexpools.com',
address: '24800 Avenue Tibbitts',
city: 'Valencia',
state: 'CA',
zip: '91355',
industry: 'Pool Service & Maintenance',
tagline: 'Pristine Pool Chemistry & Care',
yearsInBusiness: '12',
services: ['Weekly Chemical Balancing', 'Filter Repair'],
emergencyService: true,
theme: 'titan-navy',
};
const mockSections: WebsiteSection[] = [
{
id: 'sec-1',
sectionNumber: 1,
type: 'hero',
isVisible: true,
content: {
headline: 'Crystal Clear Waters & Flawless Pool Care You Can Trust',
subheadline: 'Certified CPO pool technicians delivering weekly maintenance.',
}
}
];
const testPrompts = [
{ prompt: 'theme black and yellow', expectedTheme: 'obsidian-gold' },
{ prompt: 'changing to black and yellow didnt work', expectedTheme: 'obsidian-gold' },
{ prompt: 'make it electric purple', expectedTheme: 'electric-violet' },
{ prompt: 'switch to lavender', expectedTheme: 'electric-violet' },
{ prompt: 'can we do an orange copper vibe', expectedTheme: 'sunset-amber' },
{ prompt: 'change to teal and cyan', expectedTheme: 'cyber-cyan' },
{ prompt: 'make it hot pink and black', expectedTheme: 'neon-rose' },
{ prompt: 'green and black theme', expectedTheme: 'emerald-slate' },
{ prompt: 'clean white theme', expectedTheme: 'alpine-clean' },
{ prompt: 'crimson red theme', expectedTheme: 'crimson-forge' },
{ prompt: 'change colors', expectedTheme: 'obsidian-gold' }, // default theme shift
{ prompt: 'why didnt it work', expectAiHelp: true },
];
console.log('--- RUNNING UNIVERSAL COLOR & THEME TESTS ---');
let allPassed = true;
for (const t of testPrompts) {
const result = processConversationalInstruction(t.prompt, mockSections, mockProfile);
if (t.expectedTheme) {
if (result.updatedTheme === t.expectedTheme) {
console.log(`✅ PASS: "${t.prompt}" -> ${result.updatedTheme}`);
} else {
console.error(`❌ FAIL: "${t.prompt}" -> got ${result.updatedTheme}, expected ${t.expectedTheme}`);
allPassed = false;
}
} else if (t.expectAiHelp) {
if (result.aiResponse.includes('AI Assistant Co-Pilot Support')) {
console.log(`✅ PASS: "${t.prompt}" -> Handled by Support Guard`);
} else {
console.error(`❌ FAIL: "${t.prompt}" -> fell through to copy editor!`);
allPassed = false;
}
}
// Verify that the prompt was NOT injected into hero headline/subheadline
if (result.updatedSections[0]?.content?.subheadline?.toLowerCase().includes('didnt work') ||
result.updatedSections[0]?.content?.headline?.toLowerCase().includes('didnt work')) {
console.error(`❌ CORRUPTION DETECTED: prompt text leaked into hero content for "${t.prompt}"`);
allPassed = false;
}
}
if (allPassed) {
console.log('\n🎉 ALL 12 TESTS PASSED PERFECTLY! ZERO COPY CORRUPTIONS!');
} else {
process.exit(1);
}
+87
View File
@@ -0,0 +1,87 @@
import { generateWebsiteBlueprint, processConversationalInstruction, INDUSTRY_PRESETS } from '../src/services/aiEngine';
import { BusinessProfile } from '../src/types';
console.log('--- TEST 1: GENERATE BLUEPRINT SECTION 0 ---');
const poolPreset = INDUSTRY_PRESETS['pool-service'];
const profile: BusinessProfile = {
companyName: 'Crystal Clear Pools',
phone: '(661) 555-1234',
email: 'test@aipilots.site',
address: '123 Main St',
city: 'Valencia',
state: 'CA',
zip: '91355',
industry: 'Pool Service',
tagline: 'Crystal Clear Always',
yearsInBusiness: '10',
services: ['Pool Cleaning', 'Pump Repair'],
emergencyService: true,
theme: 'titan-navy',
};
const sections = generateWebsiteBlueprint(profile);
console.log('Total sections generated:', sections.length);
const sec0 = sections.find(s => s.sectionNumber === 0);
console.log('Section 0 found:', !!sec0);
console.log('Section 0 title:', sec0?.title);
console.log('Section 0 variant:', sec0?.content.headerVariant);
console.log('Section 0 announcement:', sec0?.content.topBarAnnouncement);
console.log('Section 0 CTA:', sec0?.content.headerCtaText);
if (!sec0 || sec0.sectionNumber !== 0 || sec0.type !== 'header') {
throw new Error('Section 0 failed blueprint check');
}
console.log('\n--- TEST 2: AI INSTRUCTION -> 5-STAR CLEAN HEADER ---');
const resModern = processConversationalInstruction('make the header modern clean with 5-star rating', sections, profile);
const modSec0 = resModern.updatedSections.find(s => s.sectionNumber === 0);
console.log('AI Response:', resModern.aiResponse);
console.log('Updated Variant:', modSec0?.content.headerVariant);
console.log('Updated Announcement:', modSec0?.content.topBarAnnouncement);
console.log('Updated CTA:', modSec0?.content.headerCtaText);
if (modSec0?.content.headerVariant !== 'modern-clean') {
throw new Error('Modern clean header update failed');
}
console.log('\n--- TEST 3: AI INSTRUCTION -> EMERGENCY DISPATCH HEADER ---');
const resEmerg = processConversationalInstruction('make the header emergency dispatch with 24/7 response', resModern.updatedSections, profile);
const emergSec0 = resEmerg.updatedSections.find(s => s.sectionNumber === 0);
console.log('AI Response:', resEmerg.aiResponse);
console.log('Updated Variant:', emergSec0?.content.headerVariant);
if (emergSec0?.content.headerVariant !== 'emergency-trade') {
throw new Error('Emergency header update failed');
}
console.log('\n--- TEST 4: CUSTOM LICENSE AND TOP BAR TOGGLE ---');
const resLic = processConversationalInstruction('set header license to lic #C-53-998811', emergSec0 ? [emergSec0, ...sections.slice(1)] : sections, profile);
const licSec0 = resLic.updatedSections.find(s => s.sectionNumber === 0);
console.log('License badge:', licSec0?.content.licenseBadge);
console.log('\n--- TEST 5: USER EXACT PROMPT "ok i need a green and black landscaping app" ---');
const resLandscaping = processConversationalInstruction('ok i need a green and black landscaping app', sections, profile);
console.log('AI Response:', resLandscaping.aiResponse);
console.log('Updated Industry:', resLandscaping.updatedProfile?.industry);
console.log('Updated Company:', resLandscaping.updatedProfile?.companyName);
console.log('Updated Theme:', resLandscaping.updatedTheme);
const heroSec = resLandscaping.updatedSections.find(s => s.sectionNumber === 1);
console.log('Hero Headline:', heroSec?.content.headline);
if (!resLandscaping.updatedProfile?.industry?.includes('Landscaping')) {
throw new Error('Expected industry to contain Landscaping');
}
if (!heroSec?.content.headline?.toLowerCase().includes('landscape') && !heroSec?.content.headline?.toLowerCase().includes('outdoor')) {
throw new Error('Expected hero headline to be about landscaping');
}
console.log('\n--- TEST 6: USER FOLLOWUP PROMPT "it didnt change the wording to landscapping" ---');
const resTypo = processConversationalInstruction('it didnt change the wording to landscapping', sections, profile);
console.log('AI Response:', resTypo.aiResponse);
console.log('Updated Industry:', resTypo.updatedProfile?.industry);
console.log('Updated Company:', resTypo.updatedProfile?.companyName);
if (!resTypo.updatedProfile?.industry?.includes('Landscaping')) {
throw new Error('Expected typo landscapping to trigger landscaping overhaul');
}
console.log('\nALL 6 TESTS PASSED PERFECTLY!');
+49
View File
@@ -0,0 +1,49 @@
import { renderEnterpriseSvgIcon } from '../src/components/EnterpriseSvgIcons';
const testServices = [
'Weekly Chemical Balancing & Skimming',
'Variable Speed Pump & Filter Repair',
'Waterline Tile Cleaning & Calcium Scrub',
'Smartphone Automation & Salt Systems',
'Emergency Green-to-Clean Recovery',
'Paver Patio Installation & Hardscaping',
'Weekly Lawn Mowing & Edging',
'Sprinkler System & Irrigation Repair',
'Tree Trimming & Arborist Care',
'Low-Voltage Landscape Lighting',
'Architectural Shingle Roofing',
'Emergency Storm Tarping & Leak Repair',
'Seamless Gutters & Downspouts',
'Tankless Water Heater Installation',
'Hydro-Jet Sewer Line Cleaning',
'High-Efficiency AC Repair & Freon',
'Gas Furnace Heating Installation',
'Smart Nest Thermostat Setup',
'200A Electrical Service Panel Upgrade',
'Level 2 EV Tesla Charger Station',
'Whole-Home Standby Generator',
'Precision Laser Skin Resurfacing',
'100% Satisfaction Guarantee',
'24/7 Rapid Emergency Dispatch',
'Top Rated 5-Star Reviews',
'Transparent Flat-Rate Pricing',
];
console.log('--- TESTING ENTERPRISE SVG ICON RESOLVER ---');
let allGood = true;
for (const title of testServices) {
const iconElement = renderEnterpriseSvgIcon(title);
if (iconElement && iconElement.type) {
console.log(`✅ RESOLVED: "${title}" -> ${typeof iconElement.type === 'function' ? iconElement.type.name : 'SVG'}`);
} else {
console.error(`❌ FAILED: "${title}" did not return an icon element`);
allGood = false;
}
}
if (allGood) {
console.log('\n🎉 ALL 26 TRADE SERVICES & TRUST PILLARS RESOLVED TO BESPOKE SVG ICONS!');
} else {
process.exit(1);
}
+195
View File
@@ -0,0 +1,195 @@
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const REPLICATE_API_TOKEN = process.env.REPLICATE_API_TOKEN || 'r8_UW1EJyKwZgE7V35kfpPzh4uRJbFcvCF48vVGT';
const IMAGES_TO_GENERATE = [
// ==========================================
// ROOFING & EXTERIOR CONTRACTING
// ==========================================
{
targetPath: path.resolve(__dirname, '../public/images/roofing/hero.jpg'),
prompt: 'Commercial photography of a professional certified roofer in safety harness inspecting a pristine architectural shingle roof on a luxury modern residential house, sunny sky, crisp detail, 8k commercial photography',
aspect_ratio: '16:9',
},
{
targetPath: path.resolve(__dirname, '../public/images/roofing/shingle_replacement.jpg'),
prompt: 'Close-up commercial shot of master roofer hands installing premium charcoal architectural shingles with pneumatic nail gun on roof deck, clean straight lines, expert craftsmanship',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/roofing/drone_inspection.jpg'),
prompt: 'Commercial roof inspection drone hovering over luxury residential roof ridge, high-tech aerial survey camera, bright clear sky, precision inspection',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/roofing/gutter_install.jpg'),
prompt: 'Seamless modern black aluminum gutter system installation with downspout on clean residential roof fascia, crisp architectural detail',
aspect_ratio: '4:3',
},
// ==========================================
// PLUMBING & DRAIN SERVICES
// ==========================================
{
targetPath: path.resolve(__dirname, '../public/images/plumbing/hero.jpg'),
prompt: 'Commercial photography of a friendly master plumber in clean branded navy uniform holding a pipe wrench beside a modern service van in front of an upscale suburban home, bright daylight, 8k commercial portrait',
aspect_ratio: '16:9',
},
{
targetPath: path.resolve(__dirname, '../public/images/plumbing/tankless_heater.jpg'),
prompt: 'Modern high-efficiency wall-mounted tankless water heater installation, neat copper pipes, pressure valves, clean utility room wall, professional plumbing',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/plumbing/hydro_jetting.jpg'),
prompt: 'Commercial plumbing technician operating a high-pressure hydro-jetting rig and digital sewer camera monitor, high-tech drain diagnostic equipment',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/plumbing/pipe_repair.jpg'),
prompt: 'Plumber hands soldering copper water pipes with clean shiny joints and precision plumbing fittings under sink, expert craftsmanship, warm focused light',
aspect_ratio: '4:3',
},
// ==========================================
// MEDICAL / DENTAL / AESTHETIC CLINIC
// ==========================================
{
targetPath: path.resolve(__dirname, '../public/images/medical-legal/hero.jpg'),
prompt: 'Luxurious modern concierge wellness clinic interior, marble reception desk, warm architectural lighting, minimalist tranquil spa-like aesthetic, 8k interior design photography',
aspect_ratio: '16:9',
},
{
targetPath: path.resolve(__dirname, '../public/images/medical-legal/facial_rejuvenation.jpg'),
prompt: 'State-of-the-art aesthetic medical consultation treatment room, comfortable ergonomic procedure chair, clean modern medical equipment, elegant neutral tones',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/medical-legal/laser_treatment.jpg'),
prompt: 'Advanced modern dermatological aesthetic laser device in sleek clinic suite, high-tech wellness technology, immaculate clean environment',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/medical-legal/wellness_infusion.jpg'),
prompt: 'Serene luxury IV infusion lounge suite with plush leather armchair, soft warm lighting, calm private wellness sanctuary',
aspect_ratio: '4:3',
},
// ==========================================
// ELECTRICAL & SMART HOME SYSTEMS
// ==========================================
{
targetPath: path.resolve(__dirname, '../public/images/electrical/hero.jpg'),
prompt: 'Commercial photography of a certified master electrician in uniform using a digital multimeter inspecting a clean modern electrical service panel, bright interior, 8k commercial photography',
aspect_ratio: '16:9',
},
{
targetPath: path.resolve(__dirname, '../public/images/electrical/ev_charger.jpg'),
prompt: 'Sleek modern wall-mounted Level 2 EV car charger installed on clean garage wall charging an electric vehicle, illuminated LED indicator, premium installation',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/electrical/panel_upgrade.jpg'),
prompt: 'Clean modern 200-amp electrical breaker panel with neatly organized wiring, labeled circuit breakers, professional electrical craftsmanship',
aspect_ratio: '4:3',
},
{
targetPath: path.resolve(__dirname, '../public/images/electrical/smart_lighting.jpg'),
prompt: 'Modern architectural interior with warm recessed smart LED ceiling lighting, under-cabinet ambient lighting glow, designer luxury home',
aspect_ratio: '4:3',
},
];
async function generateSingleImage(item) {
const dir = path.dirname(item.targetPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const filename = `${path.basename(dir)}/${path.basename(item.targetPath)}`;
console.log(`[START] -> ${filename}`);
const start = Date.now();
const createRes = await fetch('https://api.replicate.com/v1/models/black-forest-labs/flux-schnell/predictions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${REPLICATE_API_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
input: {
prompt: item.prompt,
aspect_ratio: item.aspect_ratio,
output_format: 'jpg',
},
}),
});
if (!createRes.ok) {
const errText = await createRes.text();
throw new Error(`Replicate POST error (${createRes.status}): ${errText}`);
}
let prediction = await createRes.json();
const getUrl = prediction.urls.get;
let attempts = 0;
while (prediction.status !== 'succeeded' && prediction.status !== 'failed' && prediction.status !== 'canceled') {
attempts++;
await new Promise((r) => setTimeout(r, 1200));
const pollRes = await fetch(getUrl, {
headers: {
'Authorization': `Bearer ${REPLICATE_API_TOKEN}`,
},
});
if (pollRes.ok) {
prediction = await pollRes.json();
}
if (attempts > 30) {
throw new Error(`Timed out polling for ${filename}`);
}
}
if (prediction.status !== 'succeeded') {
throw new Error(`Prediction failed for ${filename}: ${JSON.stringify(prediction.error)}`);
}
const outputUrl = Array.isArray(prediction.output) ? prediction.output[0] : prediction.output;
const imgRes = await fetch(outputUrl);
if (!imgRes.ok) throw new Error(`Failed to download ${outputUrl}`);
const buffer = Buffer.from(await imgRes.arrayBuffer());
await fs.promises.writeFile(item.targetPath, buffer);
const elapsed = ((Date.now() - start) / 1000).toFixed(1);
console.log(`[DONE] -> ${filename} (${(buffer.length / 1024).toFixed(0)} KB in ${elapsed}s)`);
}
async function run() {
console.log(`🚀 Starting Replicate batch generation for ${IMAGES_TO_GENERATE.length} images across all service companies...`);
const concurrency = 4;
const queue = [...IMAGES_TO_GENERATE];
const workers = Array.from({ length: concurrency }, async (_, wId) => {
while (queue.length > 0) {
const item = queue.shift();
if (!item) break;
try {
await generateSingleImage(item);
} catch (err) {
console.error(`[FAIL] -> ${item.targetPath}:`, err.message);
}
}
});
await Promise.all(workers);
console.log('\n🎉 ALL service company images generated and saved successfully!');
}
run().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});
+333
View File
@@ -0,0 +1,333 @@
import React, { useState, useEffect } from 'react';
import { BusinessProfile, WebsiteSection, ThemeId, LeadSubmission, SectionContent, ColorMode } from './types';
import { THEMES, INDUSTRY_PRESETS, generateWebsiteBlueprint } from './services/aiEngine';
import { WebsitePreview } from './components/WebsitePreview';
import { OnboardingModal } from './components/OnboardingModal';
import { ApprovalModal } from './components/ApprovalModal';
import { AiAssistantDrawer } from './components/AiAssistantDrawer';
import { AgencyAdminDrawer } from './components/AgencyAdminDrawer';
import {
Sparkles,
Monitor,
Tablet,
Smartphone,
Bot,
Settings,
CheckCircle2,
Users,
Palette,
Layers,
ArrowRight,
Moon,
Sun
} from 'lucide-react';
export const App: React.FC = () => {
// Initial default business profile using the Pool Service preset
const defaultPreset = INDUSTRY_PRESETS['pool-service'];
const [profile, setProfile] = useState<BusinessProfile>({
companyName: defaultPreset.sampleCompany,
phone: defaultPreset.samplePhone,
email: 'brian@aipilots.site',
address: defaultPreset.sampleAddress,
city: defaultPreset.sampleCity,
state: defaultPreset.sampleState,
zip: defaultPreset.sampleZip,
industry: defaultPreset.name,
tagline: defaultPreset.sampleTagline,
yearsInBusiness: '12',
services: [...defaultPreset.defaultServices],
emergencyService: true,
theme: defaultPreset.defaultTheme,
colorMode: 'dark',
});
const [theme, setTheme] = useState<ThemeId>(defaultPreset.defaultTheme);
const [colorMode, setColorMode] = useState<ColorMode>(profile.colorMode || 'dark');
const [sections, setSections] = useState<WebsiteSection[]>(() => generateWebsiteBlueprint(profile));
const [viewportMode, setViewportMode] = useState<'desktop' | 'tablet' | 'mobile'>('desktop');
// Modal & Drawer visibility
const [isOnboardingOpen, setIsOnboardingOpen] = useState(false);
const [isApprovalOpen, setIsApprovalOpen] = useState(false);
const [isAiDrawerOpen, setIsAiDrawerOpen] = useState(true);
const [isAgencyAdminOpen, setIsAgencyAdminOpen] = useState(false);
const [targetSectionForAi, setTargetSectionForAi] = useState<number | null>(null);
const [leadCount, setLeadCount] = useState<number>(0);
// Sync document data-theme & data-color-mode
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('data-color-mode', colorMode);
}, [theme, colorMode]);
// Load existing leads count
useEffect(() => {
try {
const stored = localStorage.getItem('site_builder_leads');
if (stored) {
const parsed = JSON.parse(stored);
setLeadCount(parsed.length);
}
} catch (e) {
console.error(e);
}
}, [isApprovalOpen, isAgencyAdminOpen]);
// Handle saving new business profile from Onboarding
const handleSaveProfile = (newProfile: BusinessProfile) => {
setProfile(newProfile);
setTheme(newProfile.theme);
if (newProfile.colorMode) {
setColorMode(newProfile.colorMode);
}
const newSections = generateWebsiteBlueprint(newProfile);
setSections(newSections);
};
// Handle Quick AI Edit button click from preview section badges
const handleQuickAiEdit = (sectionNum: number) => {
setTargetSectionForAi(sectionNum);
setIsAiDrawerOpen(true);
};
// Load a submitted draft from Agency Lead Repository back into builder
const handleLoadClientDraft = (lead: LeadSubmission) => {
setProfile(lead.businessProfile);
setTheme(lead.theme);
if (lead.colorMode || lead.businessProfile.colorMode) {
setColorMode(lead.colorMode || lead.businessProfile.colorMode || 'dark');
}
setSections(lead.sections);
setIsAgencyAdminOpen(false);
};
// Handle direct inline text editing on canvas
const handleUpdateSectionContent = (sectionNumber: number, newContent: Partial<SectionContent>) => {
setSections(prev => prev.map(sec => {
if (sec.sectionNumber === sectionNumber) {
return {
...sec,
content: { ...sec.content, ...newContent }
};
}
return sec;
}));
};
return (
<div className="builder-layout">
{/* TOP APPLICATION BAR */}
<header className="app-header">
{/* Brand & Client Identity */}
<div className="app-brand-area">
<div className="brand-logo-icon">
<Sparkles size={18} />
</div>
<div className="brand-text-col">
<div className="app-title">AI Pilots Site Engine</div>
<div className="app-subtitle">
Interactive Builder <strong>{profile.companyName}</strong>
</div>
</div>
</div>
{/* Viewport Controls */}
<div className="viewport-selector">
<button
className={`viewport-btn ${viewportMode === 'desktop' ? 'active' : ''}`}
onClick={() => setViewportMode('desktop')}
title="Desktop View (1280px)"
>
<Monitor size={15} />
<span>Desktop</span>
</button>
<button
className={`viewport-btn ${viewportMode === 'tablet' ? 'active' : ''}`}
onClick={() => setViewportMode('tablet')}
title="Tablet View (820px)"
>
<Tablet size={15} />
<span>iPad / Tablet</span>
</button>
<button
className={`viewport-btn ${viewportMode === 'mobile' ? 'active' : ''}`}
onClick={() => setViewportMode('mobile')}
title="Mobile View (390px)"
>
<Smartphone size={15} />
<span>Mobile</span>
</button>
</div>
{/* Topbar Actions */}
<div className="topbar-actions">
{/* Quick Color Swatches Bar */}
<div className="topbar-swatch-bar" title="Quick Theme Switcher">
{Object.values(THEMES).filter(th => th.id !== 'custom-dynamic').map((th) => (
<button
key={th.id}
className={`topbar-swatch-btn ${theme === th.id ? 'active' : ''}`}
onClick={() => {
setTheme(th.id);
setProfile(p => ({ ...p, theme: th.id }));
}}
title={`${th.name} (${th.badge})`}
>
<span className="swatch-half-left" style={{ background: th.primary }} />
<span className="swatch-half-right" style={{ background: th.accent }} />
</button>
))}
</div>
{/* Quick Dark / Light Mode Toggle */}
<button
className="topbar-mode-toggle-btn"
onClick={() => {
const next = colorMode === 'dark' ? 'light' : 'dark';
setColorMode(next);
setProfile(p => ({ ...p, colorMode: next }));
}}
title={colorMode === 'dark' ? 'Switch to Crisp Light Mode' : 'Switch to Executive Dark Mode'}
aria-label="Toggle Dark / Light Mode"
>
{colorMode === 'dark' ? (
<>
<Sun size={15} style={{ color: '#fbbf24' }} />
<span>Light Mode</span>
</>
) : (
<>
<Moon size={15} style={{ color: '#818cf8' }} />
<span>Dark Mode</span>
</>
)}
</button>
{/* Business Profile Modal Trigger */}
<button
className="btn btn-secondary"
style={{ padding: '0.5rem 1rem', fontSize: '0.85rem' }}
onClick={() => setIsOnboardingOpen(true)}
>
<Settings size={15} />
<span>Business Setup</span>
</button>
{/* AI Co-Pilot Toggle */}
<button
className={`btn ${isAiDrawerOpen ? 'btn-primary' : 'btn-secondary'}`}
style={{ padding: '0.5rem 1rem', fontSize: '0.85rem' }}
onClick={() => setIsAiDrawerOpen(!isAiDrawerOpen)}
>
<Bot size={15} />
<span>AI Co-Pilot</span>
</button>
{/* Agency Admin Leads Trigger */}
<button
className="btn btn-secondary"
style={{ padding: '0.5rem 0.9rem', fontSize: '0.85rem', position: 'relative' }}
onClick={() => setIsAgencyAdminOpen(true)}
title="Agency Admin Drawer"
>
<Users size={15} />
<span>Leads</span>
{leadCount > 0 && (
<span
style={{
background: 'var(--accent)',
color: '#070c18',
fontSize: '0.7rem',
fontWeight: 800,
padding: '1px 6px',
borderRadius: '9999px',
marginLeft: '4px'
}}
>
{leadCount}
</span>
)}
</button>
{/* Primary Action: Approve & Submit Draft */}
<button
className="btn btn-primary"
style={{ padding: '0.55rem 1.3rem', fontSize: '0.9rem' }}
onClick={() => setIsApprovalOpen(true)}
>
<CheckCircle2 size={16} />
<span>Approve & Submit Draft</span>
</button>
</div>
</header>
{/* WORKSPACE CANVAS & AI DRAWER */}
<div className="app-container">
{/* Canvas Area */}
<div className="canvas-area">
<WebsitePreview
sections={sections}
profile={profile}
viewportMode={viewportMode}
onQuickAiEdit={handleQuickAiEdit}
onUpdateSectionContent={handleUpdateSectionContent}
/>
</div>
{/* AI Assistant Chat Drawer */}
<AiAssistantDrawer
isOpen={isAiDrawerOpen}
onClose={() => setIsAiDrawerOpen(false)}
sections={sections}
onUpdateSections={setSections}
profile={profile}
onUpdateProfile={setProfile}
onUpdateTheme={setTheme}
colorMode={colorMode}
onUpdateColorMode={setColorMode}
initialTargetSection={targetSectionForAi}
onClearTargetSection={() => setTargetSectionForAi(null)}
/>
</div>
{/* Floating AI Button if drawer is closed */}
{!isAiDrawerOpen && (
<button
className="ai-drawer-toggle-btn"
onClick={() => setIsAiDrawerOpen(true)}
>
<Bot size={20} />
<span>Ask AI to Modify Site</span>
</button>
)}
{/* MODALS */}
<OnboardingModal
isOpen={isOnboardingOpen}
onClose={() => setIsOnboardingOpen(false)}
profile={profile}
onSaveProfile={handleSaveProfile}
/>
<ApprovalModal
isOpen={isApprovalOpen}
onClose={() => setIsApprovalOpen(false)}
profile={profile}
sections={sections}
theme={theme}
colorMode={colorMode}
onSubmissionSuccess={() => {
setLeadCount(prev => prev + 1);
}}
/>
<AgencyAdminDrawer
isOpen={isAgencyAdminOpen}
onClose={() => setIsAgencyAdminOpen(false)}
onLoadClientDraft={handleLoadClientDraft}
/>
</div>
);
};
export default App;
+246
View File
@@ -0,0 +1,246 @@
import React, { useState, useEffect } from 'react';
import { LeadSubmission } from '../types';
import {
Users,
X,
Download,
Copy,
ExternalLink,
Mail,
Phone,
Building,
Check,
Globe,
Trash2,
Eye
} from 'lucide-react';
interface AgencyAdminDrawerProps {
isOpen: boolean;
onClose: () => void;
onLoadClientDraft: (lead: LeadSubmission) => void;
}
export const AgencyAdminDrawer: React.FC<AgencyAdminDrawerProps> = ({
isOpen,
onClose,
onLoadClientDraft,
}) => {
const [leads, setLeads] = useState<LeadSubmission[]>([]);
const [copiedId, setCopiedId] = useState<string | null>(null);
const loadLeads = () => {
try {
const stored = localStorage.getItem('site_builder_leads');
if (stored) {
setLeads(JSON.parse(stored));
} else {
setLeads([]);
}
} catch (e) {
console.error(e);
}
};
useEffect(() => {
if (isOpen) {
loadLeads();
}
}, [isOpen]);
const handleDeleteLead = (id: string) => {
if (confirm('Delete this client draft?')) {
const updated = leads.filter(l => l.id !== id);
setLeads(updated);
localStorage.setItem('site_builder_leads', JSON.stringify(updated));
}
};
const handleCopyLeadPayload = (lead: LeadSubmission) => {
const payload = `=== NEW SITE BUILDER LEAD ===
Client Name: ${lead.clientContact.name}
Company: ${lead.businessProfile.companyName}
Phone: ${lead.clientContact.phone}
Email: ${lead.clientContact.email}
Address: ${lead.businessProfile.address}, ${lead.businessProfile.city}, ${lead.businessProfile.state} ${lead.businessProfile.zip}
Services: ${lead.businessProfile.services.join(', ')}
Theme: ${lead.theme}
Desired Domain: ${lead.clientContact.domainPreference || 'N/A'}
Setup Requests:
- Mailcow Email: ${lead.clientContact.wantsMailcowEmail ? 'YES' : 'NO'}
- GMB Optimization: ${lead.clientContact.wantsGmb ? 'YES' : 'NO'}
- CRM Integration: ${lead.clientContact.wantsCrm ? 'YES' : 'NO'}
Notes: ${lead.clientContact.notes || 'None'}
Timestamp: ${lead.createdAt}
Agency Contact: hello@aipilots.site
=============================`;
navigator.clipboard.writeText(payload);
setCopiedId(lead.id);
setTimeout(() => setCopiedId(null), 2000);
};
const handleDownloadJson = (lead: LeadSubmission) => {
const blob = new Blob([JSON.stringify(lead, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${lead.businessProfile.companyName.toLowerCase().replace(/[^a-z0-9]/g, '-')}-blueprint.json`;
a.click();
URL.revokeObjectURL(url);
};
if (!isOpen) return null;
return (
<div className="modal-overlay" role="dialog" aria-modal="true">
<div className="modal-card" style={{ maxWidth: '880px' }}>
{/* Header */}
<div className="modal-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<div className="brand-logo-icon" style={{ width: '36px', height: '36px', background: 'linear-gradient(135deg, #4f46e5, #0284c7)' }}>
<Users size={20} />
</div>
<div>
<h2 className="modal-title">AI Pilots Agency Lead Repository</h2>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
Incoming website builder submissions & client launch blueprints ({leads.length} total)
</p>
</div>
</div>
<button className="modal-close-btn" onClick={onClose} aria-label="Close modal">
<X size={20} />
</button>
</div>
{/* Content */}
<div className="modal-body">
{leads.length === 0 ? (
<div style={{ textAlign: 'center', padding: '3rem 1rem', color: 'var(--text-muted)' }}>
<Users size={48} style={{ opacity: 0.3, marginBottom: '1rem' }} />
<p style={{ fontSize: '1.1rem', color: 'var(--text-secondary)' }}>No client drafts submitted yet.</p>
<p style={{ fontSize: '0.88rem' }}>When prospects click "Approve & Submit Draft", their complete site specs will appear here.</p>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '1.25rem' }}>
{leads.map((lead) => (
<div
key={lead.id}
style={{
background: 'rgba(255, 255, 255, 0.03)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--radius-lg)',
padding: '1.5rem'
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: '1rem' }}>
<div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.65rem' }}>
<h3 style={{ fontSize: '1.2rem', fontWeight: 800, color: '#fff' }}>
{lead.businessProfile.companyName}
</h3>
<span style={{ fontSize: '0.75rem', background: 'var(--primary)', color: '#fff', padding: '2px 8px', borderRadius: '9999px', fontWeight: 600 }}>
Theme: {lead.theme}
</span>
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)', marginTop: '0.25rem' }}>
Submitted by <strong style={{ color: '#fff' }}>{lead.clientContact.name}</strong> {new Date(lead.createdAt).toLocaleDateString()} at {new Date(lead.createdAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</div>
</div>
<div style={{ display: 'flex', gap: '0.5rem' }}>
<button
className="btn btn-secondary"
style={{ padding: '0.4rem 0.75rem', fontSize: '0.8rem' }}
onClick={() => onLoadClientDraft(lead)}
title="Load into Builder Canvas"
>
<Eye size={14} />
<span>Load Draft</span>
</button>
<button
className="btn btn-secondary"
style={{ padding: '0.4rem 0.75rem', fontSize: '0.8rem' }}
onClick={() => handleCopyLeadPayload(lead)}
title="Copy text summary"
>
{copiedId === lead.id ? <Check size={14} style={{ color: '#34d399' }} /> : <Copy size={14} />}
<span>{copiedId === lead.id ? 'Copied!' : 'Copy Summary'}</span>
</button>
<button
className="btn btn-secondary"
style={{ padding: '0.4rem 0.75rem', fontSize: '0.8rem' }}
onClick={() => handleDownloadJson(lead)}
title="Download full JSON blueprint"
>
<Download size={14} />
</button>
<button
className="btn btn-secondary"
style={{ padding: '0.4rem 0.6rem', fontSize: '0.8rem', color: '#f87171' }}
onClick={() => handleDeleteLead(lead.id)}
title="Delete Lead"
>
<Trash2 size={14} />
</button>
</div>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: '1rem', background: 'rgba(0, 0, 0, 0.2)', padding: '1rem', borderRadius: 'var(--radius-md)', fontSize: '0.85rem' }}>
<div>
<div style={{ color: 'var(--text-muted)', fontSize: '0.75rem', textTransform: 'uppercase' }}>Contact Info</div>
<div style={{ color: '#fff', marginTop: '0.2rem' }}>📞 {lead.clientContact.phone}</div>
<div style={{ color: 'var(--accent)', marginTop: '0.1rem' }}> {lead.clientContact.email}</div>
</div>
<div>
<div style={{ color: 'var(--text-muted)', fontSize: '0.75rem', textTransform: 'uppercase' }}>Service Location</div>
<div style={{ color: '#fff', marginTop: '0.2rem' }}>📍 {lead.businessProfile.city}, {lead.businessProfile.state}</div>
<div style={{ color: 'var(--text-secondary)', marginTop: '0.1rem' }}>{lead.businessProfile.address}</div>
</div>
<div>
<div style={{ color: 'var(--text-muted)', fontSize: '0.75rem', textTransform: 'uppercase' }}>Requested Setup</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.35rem', marginTop: '0.3rem' }}>
{lead.clientContact.wantsMailcowEmail && (
<span style={{ fontSize: '0.72rem', background: 'rgba(52, 211, 153, 0.15)', color: '#34d399', padding: '2px 6px', borderRadius: '4px' }}>
Mailcow Email
</span>
)}
{lead.clientContact.wantsGmb && (
<span style={{ fontSize: '0.72rem', background: 'rgba(56, 189, 248, 0.15)', color: 'var(--accent)', padding: '2px 6px', borderRadius: '4px' }}>
GMB 3-Pack
</span>
)}
{lead.clientContact.wantsCrm && (
<span style={{ fontSize: '0.72rem', background: 'rgba(251, 191, 36, 0.15)', color: '#fbbf24', padding: '2px 6px', borderRadius: '4px' }}>
CRM SMS Sync
</span>
)}
</div>
</div>
</div>
{lead.clientContact.notes && (
<div style={{ marginTop: '0.75rem', fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
<strong style={{ color: '#fff' }}>Client Notes:</strong> {lead.clientContact.notes}
</div>
)}
</div>
))}
</div>
)}
</div>
<div className="modal-footer">
<button className="btn btn-secondary" onClick={onClose}>
Close
</button>
</div>
</div>
</div>
);
};
+310
View File
@@ -0,0 +1,310 @@
import React, { useState, useRef, useEffect } from 'react';
import { ChatMessage, WebsiteSection, BusinessProfile, ThemeId, ColorMode } from '../types';
import { processConversationalInstruction } from '../services/aiEngine';
import {
Send,
Sparkles,
X,
Bot,
User,
Wand2,
Check,
Lightbulb,
ChevronDown,
ChevronUp
} from 'lucide-react';
interface AiAssistantDrawerProps {
isOpen: boolean;
onClose: () => void;
sections: WebsiteSection[];
onUpdateSections: (newSections: WebsiteSection[]) => void;
profile: BusinessProfile;
onUpdateProfile: (newProfile: BusinessProfile) => void;
onUpdateTheme: (theme: ThemeId) => void;
colorMode?: ColorMode;
onUpdateColorMode?: (mode: ColorMode) => void;
initialTargetSection?: number | null;
onClearTargetSection?: () => void;
}
export const AiAssistantDrawer: React.FC<AiAssistantDrawerProps> = ({
isOpen,
onClose,
sections,
onUpdateSections,
profile,
onUpdateProfile,
onUpdateTheme,
colorMode,
onUpdateColorMode,
initialTargetSection,
onClearTargetSection,
}) => {
const [messages, setMessages] = useState<ChatMessage[]>([
{
id: 'msg-welcome',
sender: 'ai',
text: `👋 Hi! I'm your **AI Website Architect**.
You do **NOT** need to click buttons or pills — just type what you want in plain English! For example:
- *"add commercial stock images"*
- *"make for a pool company instead"*
- *"the entire site needs to be made for a landscaper green and black"*
- *"I don't like section 3, make it focus on 24/7 emergency dispatch"*
- *"make it gold and black"*
- *"change our phone to (555) 234-5678"*`,
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
},
]);
const [inputVal, setInputVal] = useState('');
const [isTyping, setIsTyping] = useState(false);
const [showSuggestions, setShowSuggestions] = useState(true);
const chatEndRef = useRef<HTMLDivElement>(null);
// Auto-fill prompt when quick edit is triggered from preview
useEffect(() => {
if (initialTargetSection) {
setInputVal(`I don't like section ${initialTargetSection}, make it like `);
if (onClearTargetSection) onClearTargetSection();
}
}, [initialTargetSection, onClearTargetSection]);
// Scroll to bottom of chat
useEffect(() => {
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, isTyping]);
const handleSendMessage = (textToSend?: string) => {
const text = (textToSend || inputVal).trim();
if (!text) return;
const userMsg: ChatMessage = {
id: `msg-${Date.now()}`,
sender: 'user',
text,
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
};
setMessages(prev => [...prev, userMsg]);
setInputVal('');
setIsTyping(true);
// Process natural AI instructions
setTimeout(() => {
const result = processConversationalInstruction(text, sections, profile);
// Apply modifications to canvas state
onUpdateSections(result.updatedSections);
if (result.updatedProfile) {
onUpdateProfile({ ...profile, ...result.updatedProfile });
if (result.updatedProfile.colorMode && onUpdateColorMode) {
onUpdateColorMode(result.updatedProfile.colorMode);
}
}
if (result.updatedTheme) {
onUpdateTheme(result.updatedTheme);
}
// Smooth scroll canvas
if (result.modifiedSectionNumber === 1) {
document.querySelector('.canvas-area')?.scrollTo({ top: 0, behavior: 'smooth' });
} else if (result.modifiedSectionNumber) {
const secEl = document.getElementById(`section-${result.modifiedSectionNumber}`);
if (secEl) {
secEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}
const aiMsg: ChatMessage = {
id: `msg-${Date.now() + 1}`,
sender: 'ai',
text: result.aiResponse,
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
modifiedSectionNumber: result.modifiedSectionNumber,
};
setMessages(prev => [...prev, aiMsg]);
setIsTyping(false);
}, 400);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && !e.nativeEvent.isComposing) {
e.preventDefault();
handleSendMessage();
}
};
if (!isOpen) return null;
return (
<aside className="ai-drawer-container">
{/* Header */}
<div className="ai-drawer-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<div className="ai-avatar">
<Bot size={20} />
</div>
<div className="ai-header-text">
<h3>AI Co-Pilot & Section Editor</h3>
<span className="ai-status-pill">
<span className="ai-status-dot"></span>
Ready Type any natural request
</span>
</div>
</div>
<button
className="modal-close-btn"
onClick={onClose}
aria-label="Close Assistant"
>
<X size={18} />
</button>
</div>
{/* Chat Messages Log */}
<div className="chat-history-container">
{messages.map((msg) => (
<div key={msg.id} className={`chat-bubble ${msg.sender}`}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem', marginBottom: '0.25rem', opacity: 0.7, fontSize: '0.72rem' }}>
{msg.sender === 'ai' ? <Sparkles size={12} /> : <User size={12} />}
<span>{msg.sender === 'ai' ? 'AI Architect' : 'You'}</span>
<span></span>
<span>{msg.timestamp}</span>
</div>
<div style={{ whiteSpace: 'pre-wrap' }}>{msg.text}</div>
{msg.modifiedSectionNumber && (
<button
className="chat-action-btn"
onClick={() => {
if (msg.modifiedSectionNumber === 1) {
document.querySelector('.canvas-area')?.scrollTo({ top: 0, behavior: 'smooth' });
} else {
const el = document.getElementById(`section-${msg.modifiedSectionNumber}`);
el?.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}}
>
<Check size={14} />
<span>Jump to Section {msg.modifiedSectionNumber} in Preview</span>
</button>
)}
</div>
))}
{isTyping && (
<div className="chat-bubble ai" style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--accent)' }}>
<Wand2 size={16} className="animate-spin" />
<span>Refactoring website blueprint...</span>
</div>
)}
<div ref={chatEndRef} />
</div>
{/* Sleek Always-Visible Quick AI Action Chips */}
<div className="quick-action-chips-container">
<div className="quick-action-chips-scroll">
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("switch to light mode")}
>
Light Mode
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("switch to dark mode")}
>
🌙 Dark Mode
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("theme black and yellow")}
>
🟡 Black & Yellow
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("make it electric purple")}
>
🟣 Electric Violet
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("can we do an orange copper vibe")}
>
🟠 Sunset Amber
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("change to teal and cyan")}
>
🔵 Cyber Cyan
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("the entire site needs to be made for a landscaper green and black")}
>
🌿 Landscaping (Green)
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("make for a pool company instead")}
>
🏊 Pool Care Site
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("add commercial stock images to the site")}
>
📸 Add Stock Photos
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("Add 15% first-time customer discount to CTA banner")}
>
💰 15% Promo Banner
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("make the header emergency style with 24/7 dispatch")}
>
Emergency Header
</button>
<button
className="quick-chip-pill"
onClick={() => handleSendMessage("make header modern clean with 5-star rating")}
>
5-Star Clean Header
</button>
</div>
</div>
{/* Input Bar */}
<div className="chat-input-bar">
<div className="chat-input-wrapper">
<input
type="text"
className="chat-text-input"
placeholder="Type anything (e.g. 'make for a pool company instead')"
value={inputVal}
onChange={(e) => setInputVal(e.target.value)}
onKeyDown={handleKeyDown}
/>
<button
className="chat-send-btn"
onClick={() => handleSendMessage()}
disabled={!inputVal.trim()}
aria-label="Send Message"
>
<Send size={16} />
</button>
</div>
</div>
</aside>
);
};
+353
View File
@@ -0,0 +1,353 @@
import React, { useState } from 'react';
import { BusinessProfile, WebsiteSection, ThemeId, LeadSubmission, ColorMode } from '../types';
import {
CheckCircle2,
X,
Send,
Download,
Mail,
Globe,
MapPin,
Sparkles,
ShieldCheck,
Check,
Building,
ArrowRight
} from 'lucide-react';
interface ApprovalModalProps {
isOpen: boolean;
onClose: () => void;
profile: BusinessProfile;
sections: WebsiteSection[];
theme: ThemeId;
colorMode?: ColorMode;
onSubmissionSuccess: (lead: LeadSubmission) => void;
}
export const ApprovalModal: React.FC<ApprovalModalProps> = ({
isOpen,
onClose,
profile,
sections,
theme,
colorMode,
onSubmissionSuccess,
}) => {
const [clientName, setClientName] = useState('');
const [clientEmail, setClientEmail] = useState(profile.email || '');
const [clientPhone, setClientPhone] = useState(profile.phone || '');
const [domainPreference, setDomainPreference] = useState('');
const [notes, setNotes] = useState('');
const [wantsGmb, setWantsGmb] = useState(true);
const [wantsMailcowEmail, setWantsMailcowEmail] = useState(true);
const [wantsCrm, setWantsCrm] = useState(true);
const [isSubmitted, setIsSubmitted] = useState(false);
const [submittedLead, setSubmittedLead] = useState<LeadSubmission | null>(null);
if (!isOpen) return null;
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
const newLead: LeadSubmission = {
id: `lead-${Date.now()}`,
createdAt: new Date().toISOString(),
businessProfile: profile,
clientContact: {
name: clientName,
email: clientEmail,
phone: clientPhone,
domainPreference,
notes,
wantsGmb,
wantsMailcowEmail,
wantsCrm,
},
sections,
theme,
colorMode: colorMode || profile.colorMode || 'dark',
status: 'pending_review',
};
// Save to localStorage repository
try {
const existing = localStorage.getItem('site_builder_leads');
const leadsList: LeadSubmission[] = existing ? JSON.parse(existing) : [];
leadsList.unshift(newLead);
localStorage.setItem('site_builder_leads', JSON.stringify(leadsList));
} catch (err) {
console.error('Error saving lead locally:', err);
}
setSubmittedLead(newLead);
setIsSubmitted(true);
onSubmissionSuccess(newLead);
};
const handleDownloadBlueprint = () => {
if (!submittedLead) return;
const blob = new Blob([JSON.stringify(submittedLead, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${profile.companyName.toLowerCase().replace(/[^a-z0-9]/g, '-')}-site-blueprint.json`;
a.click();
URL.revokeObjectURL(url);
};
return (
<div className="modal-overlay" role="dialog" aria-modal="true">
<div className="modal-card" style={{ maxWidth: '680px' }}>
{/* Modal Header */}
<div className="modal-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<div className="brand-logo-icon" style={{ width: '36px', height: '36px' }}>
<CheckCircle2 size={20} />
</div>
<div>
<h2 className="modal-title">Approve & Launch My Site</h2>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
Step 2: Submit your draft to lock in custom domain, Mailcow email, GMB & CRM setup
</p>
</div>
</div>
<button className="modal-close-btn" onClick={onClose} aria-label="Close modal">
<X size={20} />
</button>
</div>
{/* Modal Content */}
<div className="modal-body">
{isSubmitted ? (
/* SUCCESS CONFIRMATION STATE */
<div style={{ textAlign: 'center', padding: '1rem 0' }}>
<div
style={{
width: '64px',
height: '64px',
borderRadius: '50%',
background: 'rgba(52, 211, 153, 0.15)',
color: '#34d399',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
margin: '0 auto 1.5rem',
boxShadow: '0 0 25px rgba(52, 211, 153, 0.3)'
}}
>
<Check size={36} />
</div>
<h3 style={{ fontFamily: 'var(--font-heading)', fontSize: '1.6rem', fontWeight: 800, color: '#fff', marginBottom: '0.75rem' }}>
Draft Approved & Successfully Submitted!
</h3>
<p style={{ color: 'var(--text-secondary)', fontSize: '1rem', lineHeight: 1.6, maxWidth: '520px', margin: '0 auto 2rem' }}>
Your complete homepage architecture for <strong style={{ color: '#fff' }}>{profile.companyName}</strong> has been transmitted to our engineering team at <strong style={{ color: 'var(--accent)' }}>hello@aipilots.site</strong>.
</p>
{/* Agency Launch Pipeline Box */}
<div className="roadmap-box" style={{ textAlign: 'left', marginBottom: '2rem' }}>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: 'var(--accent)', textTransform: 'uppercase', marginBottom: '1rem' }}>
Your Deployment Roadmap:
</div>
<div className="roadmap-step">
<div className="roadmap-step-icon">1</div>
<div className="roadmap-step-content">
<h4>Custom Domain & Cloudflare DNS</h4>
<p>Securing {domainPreference || `${profile.companyName.toLowerCase().replace(/\s+/g, '')}.com`} with SSL and edge caching.</p>
</div>
</div>
<div className="roadmap-step">
<div className="roadmap-step-icon">2</div>
<div className="roadmap-step-content">
<h4>Professional Mailcow Business Email</h4>
<p>Setting up verified SPF, DKIM, DMARC inboxes for your domain.</p>
</div>
</div>
<div className="roadmap-step">
<div className="roadmap-step-icon">3</div>
<div className="roadmap-step-content">
<h4>Google Business Profile (GMB) Optimization</h4>
<p>Connecting your address citation and Google Map Pack ranking strategy.</p>
</div>
</div>
<div className="roadmap-step">
<div className="roadmap-step-icon">4</div>
<div className="roadmap-step-content">
<h4>CRM & Lead Notification Routing</h4>
<p>Hooking all quote forms directly to your phone ({clientPhone}) and email.</p>
</div>
</div>
</div>
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center' }}>
<button
type="button"
className="btn btn-primary"
onClick={handleDownloadBlueprint}
>
<Download size={16} />
<span>Download Site Blueprint (.JSON)</span>
</button>
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
>
Done
</button>
</div>
</div>
) : (
/* SUBMISSION FORM */
<form onSubmit={handleSubmit}>
<div style={{
background: 'rgba(56, 189, 248, 0.05)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--radius-lg)',
padding: '1.25rem',
marginBottom: '1.75rem'
}}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.5rem' }}>
<div style={{ fontWeight: 700, color: '#fff', fontSize: '1.05rem' }}>
{profile.companyName || 'Your Business'}
</div>
<span style={{ fontSize: '0.78rem', background: 'var(--primary)', color: '#fff', padding: '2px 8px', borderRadius: '9999px', fontWeight: 600 }}>
Theme: {theme} {colorMode === 'light' ? '☀️ Light' : '🌙 Dark'}
</span>
</div>
<div style={{ fontSize: '0.85rem', color: 'var(--text-secondary)' }}>
{profile.services.length} services configured {profile.city}, {profile.state} Phone: {profile.phone}
</div>
</div>
<div className="form-grid" style={{ marginTop: 0 }}>
{/* Contact Name */}
<div>
<label className="form-label" htmlFor="contact-name">Your Full Name (Owner / Decision Maker) *</label>
<input
id="contact-name"
type="text"
className="form-input"
placeholder="e.g. Sarah Connor"
value={clientName}
onChange={(e) => setClientName(e.target.value)}
required
autoComplete="name"
/>
</div>
{/* Contact Email */}
<div>
<label className="form-label" htmlFor="contact-email">Email Address for Site Launch *</label>
<input
id="contact-email"
type="email"
className="form-input"
placeholder="sarah@example.com"
value={clientEmail}
onChange={(e) => setClientEmail(e.target.value)}
required
autoComplete="email"
/>
</div>
{/* Contact Phone */}
<div>
<label className="form-label" htmlFor="contact-phone">Direct Mobile / Work Phone *</label>
<input
id="contact-phone"
type="tel"
className="form-input"
placeholder="(555) 000-0000"
value={clientPhone}
onChange={(e) => setClientPhone(e.target.value)}
required
autoComplete="tel"
/>
</div>
{/* Domain Preference */}
<div>
<label className="form-label" htmlFor="domain-pref">Desired Domain Name (Optional)</label>
<input
id="domain-pref"
type="text"
className="form-input"
placeholder="e.g. apexpoolpros.com"
value={domainPreference}
onChange={(e) => setDomainPreference(e.target.value)}
/>
</div>
{/* Onboarding Checklist Options */}
<div className="form-group-full" style={{ borderTop: '1px solid var(--border-subtle)', paddingTop: '1.25rem', marginTop: '0.5rem' }}>
<div style={{ fontSize: '0.9rem', fontWeight: 700, color: '#fff', marginBottom: '0.75rem' }}>
Select What You Want AI Pilots to Provision For You:
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.65rem' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', cursor: 'pointer', fontSize: '0.88rem' }}>
<input
type="checkbox"
checked={wantsMailcowEmail}
onChange={(e) => setWantsMailcowEmail(e.target.checked)}
style={{ width: '16px', height: '16px', accentColor: 'var(--primary)' }}
/>
<span>Professional Business Email Inboxes (Mailcow high-deliverability infrastructure)</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', cursor: 'pointer', fontSize: '0.88rem' }}>
<input
type="checkbox"
checked={wantsGmb}
onChange={(e) => setWantsGmb(e.target.checked)}
style={{ width: '16px', height: '16px', accentColor: 'var(--primary)' }}
/>
<span>Google Business Profile (GMB) Verification & Local Map 3-Pack Optimization</span>
</label>
<label style={{ display: 'flex', alignItems: 'center', gap: '0.6rem', cursor: 'pointer', fontSize: '0.88rem' }}>
<input
type="checkbox"
checked={wantsCrm}
onChange={(e) => setWantsCrm(e.target.checked)}
style={{ width: '16px', height: '16px', accentColor: 'var(--primary)' }}
/>
<span>Client Lead CRM & Instant SMS Call Forwarding Integration</span>
</label>
</div>
</div>
{/* Notes */}
<div className="form-group-full">
<label className="form-label" htmlFor="launch-notes">Special Instructions or Custom Requests</label>
<textarea
id="launch-notes"
className="form-textarea"
placeholder="Any specific logo colors, custom domain access, or timeline goals..."
value={notes}
onChange={(e) => setNotes(e.target.value)}
rows={3}
/>
</div>
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: '1rem', marginTop: '2rem' }}>
<button type="button" className="btn btn-secondary" onClick={onClose}>
Back to Preview
</button>
<button type="submit" className="btn btn-primary" style={{ padding: '0.85rem 2rem' }}>
<Send size={16} />
<span>Submit Final Draft</span>
</button>
</div>
</form>
)}
</div>
</div>
</div>
);
};
+386
View File
@@ -0,0 +1,386 @@
import React from 'react';
interface SvgIconProps {
size?: number;
className?: string;
primaryColor?: string;
accentColor?: string;
}
// 1. POOL SERVICE ICONS
export const PoolChemistrySvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 3C16 3 8 13.5 8 19.5C8 23.9183 11.5817 27.5 16 27.5C20.4183 27.5 24 23.9183 24 19.5C24 13.5 16 3 16 3Z" fill="currentColor" fillOpacity="0.16" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<path d="M12 20C12 22.2091 13.7909 24 16 24" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<circle cx="16" cy="12" r="1.5" fill="var(--accent, currentColor)"/>
<circle cx="19" cy="17" r="1" fill="var(--accent, currentColor)"/>
<path d="M16 8V10M20 11L18.5 12M12 11L13.5 12" stroke="var(--accent, currentColor)" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
);
export const PoolPumpSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="16" cy="16" r="11" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2"/>
<circle cx="16" cy="16" r="4" fill="var(--accent, currentColor)" fillOpacity="0.3" stroke="var(--accent, currentColor)" strokeWidth="2"/>
<path d="M16 5V12M16 20V27M5 16H12M20 16H27" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M8.22 8.22L13.17 13.17M18.83 18.83L23.78 23.78M23.78 8.22L18.83 13.17M13.17 18.83L8.22 23.78" stroke="var(--accent, currentColor)" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
);
export const SparklingTileSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="5" y="5" width="10" height="10" rx="2" fill="currentColor" fillOpacity="0.16" stroke="currentColor" strokeWidth="2"/>
<rect x="17" y="5" width="10" height="10" rx="2" fill="currentColor" fillOpacity="0.28" stroke="currentColor" strokeWidth="2"/>
<rect x="5" y="17" width="10" height="10" rx="2" fill="currentColor" fillOpacity="0.28" stroke="currentColor" strokeWidth="2"/>
<rect x="17" y="17" width="10" height="10" rx="2" fill="currentColor" fillOpacity="0.16" stroke="currentColor" strokeWidth="2"/>
<path d="M22 2V6M20 4H24M8 26V30M6 28H10" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round"/>
<circle cx="22" cy="10" r="1.5" fill="var(--accent, currentColor)"/>
<circle cx="10" cy="22" r="1.5" fill="var(--accent, currentColor)"/>
</svg>
);
export const SmartAutomationSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="9" y="4" width="14" height="24" rx="3" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2"/>
<line x1="13" y1="8" x2="19" y2="8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"/>
<circle cx="16" cy="24" r="1.5" fill="currentColor"/>
<path d="M4 11C6.2 8.5 9.5 7 13 7" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round"/>
<path d="M28 11C25.8 8.5 22.5 7 19 7" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round"/>
<path d="M6 15C7.5 13.5 9.5 12.5 12 12.5" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round"/>
<path d="M26 15C24.5 13.5 22.5 12.5 20 12.5" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round"/>
<path d="M14 14L18 18M18 14L14 18" stroke="var(--accent, currentColor)" strokeWidth="1.5" strokeLinecap="round"/>
</svg>
);
export const CrystalRescueSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 3L27 7.5V16C27 22.5 22 27.5 16 29C10 27.5 5 22.5 5 16V7.5L16 3Z" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<path d="M16 9L18 14H23L19 17.5L20.5 22.5L16 19.5L11.5 22.5L13 17.5L9 14H14L16 9Z" fill="var(--accent, currentColor)" fillOpacity="0.3" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinejoin="round"/>
</svg>
);
// 2. LANDSCAPING & HARDSCAPE ICONS
export const PaverMasonrySvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="4" y="6" width="11" height="8" rx="1.5" fill="currentColor" fillOpacity="0.18" stroke="currentColor" strokeWidth="2"/>
<rect x="17" y="6" width="11" height="8" rx="1.5" fill="currentColor" fillOpacity="0.18" stroke="currentColor" strokeWidth="2"/>
<rect x="9" y="16" width="14" height="9" rx="1.5" fill="var(--accent, currentColor)" fillOpacity="0.25" stroke="var(--accent, currentColor)" strokeWidth="2"/>
<path d="M4 16H7M25 16H28" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M4 25H7M25 25H28" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
export const PrecisionLawnSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M4 27H28" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M6 26C7 19 10 13 12 7C12.5 13 13.5 20 14 26" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M14 26C15 17 18 10 20 5C20.5 12 21.5 19 22 26" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="var(--accent, currentColor)" fillOpacity="0.18"/>
<path d="M22 26C23 20 25 15 27 11" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
export const IrrigationSpraySvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="13" y="19" width="6" height="9" rx="1" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="2"/>
<line x1="8" y1="28" x2="24" y2="28" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M16 19V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M16 10C16 10 11 8 8 5" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<path d="M16 10C16 10 21 8 24 5" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<path d="M16 8V3" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<circle cx="8" cy="4" r="1.5" fill="var(--accent, currentColor)"/>
<circle cx="24" cy="4" r="1.5" fill="var(--accent, currentColor)"/>
<circle cx="16" cy="2" r="1.5" fill="var(--accent, currentColor)"/>
</svg>
);
export const TreeArboristSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 28V18M16 22L20 18M16 20L12 17" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"/>
<path d="M16 4C11.58 4 8 7.58 8 12C8 13.8 8.6 15.46 9.62 16.8C10.6 18.08 12.18 19 14 19.3V19.5C14 20.33 14.67 21 15.5 21H16.5C17.33 21 18 20.33 18 19.5V19.3C19.82 19 21.4 18.08 22.38 16.8C23.4 15.46 24 13.8 24 12C24 7.58 20.42 4 16 4Z" fill="currentColor" fillOpacity="0.18" stroke="currentColor" strokeWidth="2"/>
<path d="M16 7C13.5 7 11.5 9 11.5 11.5" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round"/>
</svg>
);
export const LandscapeLightingSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 27V15" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M10 15H22L19 9H13L10 15Z" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<path d="M8 27H24" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M13 16L9 24M19 16L23 24M16 16V25" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round" strokeDasharray="2 2"/>
<circle cx="16" cy="9" r="1.5" fill="var(--accent, currentColor)"/>
</svg>
);
// 3. ROOFING ICONS
export const ArchitecturalRoofSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M3 14L16 4L29 14" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M6 14V26H26V14" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" fill="currentColor" fillOpacity="0.12"/>
<line x1="6" y1="18" x2="26" y2="18" stroke="var(--accent, currentColor)" strokeWidth="1.75"/>
<line x1="6" y1="22" x2="26" y2="22" stroke="var(--accent, currentColor)" strokeWidth="1.75"/>
<line x1="12" y1="14" x2="12" y2="18" stroke="currentColor" strokeWidth="1.5"/>
<line x1="20" y1="14" x2="20" y2="18" stroke="currentColor" strokeWidth="1.5"/>
<line x1="16" y1="18" x2="16" y2="22" stroke="currentColor" strokeWidth="1.5"/>
<line x1="10" y1="22" x2="10" y2="26" stroke="currentColor" strokeWidth="1.5"/>
<line x1="22" y1="22" x2="22" y2="26" stroke="currentColor" strokeWidth="1.5"/>
</svg>
);
export const StormShieldSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 3L27 7.5V16C27 22.5 22 27.5 16 29C10 27.5 5 22.5 5 16V7.5L16 3Z" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<path d="M18 10L12 18H17L14 24L21 15H16L18 10Z" fill="var(--accent, currentColor)" stroke="var(--accent, currentColor)" strokeWidth="1.5" strokeLinejoin="round"/>
</svg>
);
export const SeamlessGuttersSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M4 8H24V14C24 16.2 22.2 18 20 18H8C5.8 18 4 16.2 4 14V8Z" fill="currentColor" fillOpacity="0.16" stroke="currentColor" strokeWidth="2"/>
<path d="M20 18V26C20 27.1 20.9 28 22 28H26" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M10 13L10 15M14 13L14 15M18 13L18 15" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<circle cx="25" cy="24" r="1.5" fill="var(--accent, currentColor)"/>
</svg>
);
// 4. PLUMBING ICONS
export const TanklessHeaterSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="7" y="4" width="18" height="24" rx="3" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2"/>
<circle cx="16" cy="12" r="4" stroke="currentColor" strokeWidth="1.75"/>
<path d="M16 10V12L17.5 13" stroke="var(--accent, currentColor)" strokeWidth="1.5" strokeLinecap="round"/>
<path d="M16 19C16 19 13.5 21 13.5 22.5C13.5 23.88 14.62 25 16 25C17.38 25 18.5 23.88 18.5 22.5C18.5 21 16 19 16 19Z" fill="var(--accent, currentColor)" stroke="var(--accent, currentColor)" strokeWidth="1.5"/>
<line x1="11" y1="28" x2="11" y2="30" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<line x1="21" y1="28" x2="21" y2="30" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
export const HydroJetNozzleSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M5 16H16" stroke="currentColor" strokeWidth="4" strokeLinecap="round"/>
<path d="M16 11L25 16L16 21V11Z" fill="currentColor" fillOpacity="0.2" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<path d="M26 12L29 9M27 16H31M26 20L29 23" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<path d="M18 9L15 6M18 23L15 26" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round"/>
</svg>
);
export const PipeValveSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<line x1="4" y1="16" x2="28" y2="16" stroke="currentColor" strokeWidth="3" strokeLinecap="round"/>
<circle cx="16" cy="16" r="5" fill="currentColor" fillOpacity="0.18" stroke="currentColor" strokeWidth="2"/>
<path d="M16 11V5M11 5H21" stroke="var(--accent, currentColor)" strokeWidth="2.5" strokeLinecap="round"/>
<circle cx="16" cy="16" r="2" fill="var(--accent, currentColor)"/>
</svg>
);
// 5. HVAC ICONS
export const AcCoolingSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<line x1="16" y1="4" x2="16" y2="28" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<line x1="4" y1="16" x2="28" y2="16" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<line x1="7.5" y1="7.5" x2="24.5" y2="24.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<line x1="7.5" y1="24.5" x2="24.5" y2="7.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<circle cx="16" cy="16" r="3" fill="var(--accent, currentColor)" fillOpacity="0.3" stroke="var(--accent, currentColor)" strokeWidth="2"/>
<path d="M13 6L16 4L19 6M26 13L28 16L26 19M19 26L16 28L13 26M6 19L4 16L6 13" stroke="var(--accent, currentColor)" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
export const HeatingBurnerSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 4C16 4 23 11 23 18C23 22.4 19.9 26 16 26C12.1 26 9 22.4 9 18C9 14.5 12 10.5 16 4Z" fill="currentColor" fillOpacity="0.18" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<path d="M16 14C16 14 19 17.5 19 20C19 21.66 17.66 23 16 23C14.34 23 13 21.66 13 20C13 18.5 14.5 16.5 16 14Z" fill="var(--accent, currentColor)" stroke="var(--accent, currentColor)" strokeWidth="1.5" strokeLinejoin="round"/>
<line x1="6" y1="29" x2="26" y2="29" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
export const SmartThermostatSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="16" cy="16" r="12" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2"/>
<circle cx="16" cy="16" r="8" stroke="var(--accent, currentColor)" strokeWidth="1.5" strokeDasharray="3 3"/>
<path d="M13 16C13 14.34 14.34 13 16 13C17.66 13 19 14.34 19 16" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<circle cx="16" cy="16" r="2" fill="var(--accent, currentColor)"/>
<circle cx="21" cy="11" r="1" fill="var(--accent, currentColor)"/>
</svg>
);
// 6. ELECTRICAL ICONS
export const ElectricPanelSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="7" y="4" width="18" height="24" rx="2" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2"/>
<line x1="16" y1="4" x2="16" y2="28" stroke="currentColor" strokeWidth="1.5"/>
<rect x="9" y="8" width="5" height="3" rx="1" fill="var(--accent, currentColor)"/>
<rect x="18" y="8" width="5" height="3" rx="1" fill="currentColor" fillOpacity="0.3"/>
<rect x="9" y="14" width="5" height="3" rx="1" fill="currentColor" fillOpacity="0.3"/>
<rect x="18" y="14" width="5" height="3" rx="1" fill="var(--accent, currentColor)"/>
<rect x="9" y="20" width="5" height="3" rx="1" fill="var(--accent, currentColor)"/>
<rect x="18" y="20" width="5" height="3" rx="1" fill="currentColor" fillOpacity="0.3"/>
<circle cx="22" cy="6" r="1" fill="currentColor"/>
</svg>
);
export const EvChargerSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="6" y="5" width="12" height="22" rx="2" fill="currentColor" fillOpacity="0.15" stroke="currentColor" strokeWidth="2"/>
<path d="M18 11H23C24.1 11 25 11.9 25 13V22C25 23.1 24.1 24 23 24H21" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<path d="M12 9L9 15H14L11 21" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<circle cx="24" cy="18" r="1.5" fill="var(--accent, currentColor)"/>
</svg>
);
export const GeneratorTurbineSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<rect x="5" y="7" width="22" height="18" rx="3" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2"/>
<circle cx="16" cy="16" r="5" fill="var(--accent, currentColor)" fillOpacity="0.2" stroke="var(--accent, currentColor)" strokeWidth="2"/>
<line x1="16" y1="12" x2="16" y2="20" stroke="var(--accent, currentColor)" strokeWidth="1.75"/>
<line x1="12" y1="16" x2="20" y2="16" stroke="var(--accent, currentColor)" strokeWidth="1.75"/>
<line x1="8" y1="28" x2="11" y2="25" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
<line x1="24" y1="28" x2="21" y2="25" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
// 7. TRUST & PERFORMANCE BADGE ICONS
export const RoyalShieldGuaranteeSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 3L27 7.5V16C27 22.5 22 27.5 16 29C10 27.5 5 22.5 5 16V7.5L16 3Z" fill="currentColor" fillOpacity="0.16" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<path d="M11 16L14.5 19.5L21.5 12.5" stroke="var(--accent, currentColor)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
export const RapidDispatchSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="16" cy="17" r="11" fill="currentColor" fillOpacity="0.14" stroke="currentColor" strokeWidth="2"/>
<path d="M16 11V17L20 19" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M13 3H19M16 3V6" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<path d="M25 8L27 6" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
export const FiveStarLaurelSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M16 6L18.8 11.7L25 12.6L20.5 17L21.6 23.2L16 20.2L10.4 23.2L11.5 17L7 12.6L13.2 11.7L16 6Z" fill="var(--accent, currentColor)" fillOpacity="0.25" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinejoin="round"/>
<path d="M6 26C6 26 8 28 12 28M26 26C26 26 24 28 20 28" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
export const TransparentPricingSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<path d="M5 13L16 2L27 13L16 24L5 13Z" fill="currentColor" fillOpacity="0.16" stroke="currentColor" strokeWidth="2" strokeLinejoin="round"/>
<circle cx="16" cy="13" r="3" fill="var(--accent, currentColor)" stroke="var(--accent, currentColor)" strokeWidth="1.5"/>
<path d="M16 24V29M12 29H20" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
export const AestheticLaserSvg: React.FC<SvgIconProps> = ({ size = 28, className }) => (
<svg width={size} height={size} viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" className={className}>
<circle cx="16" cy="16" r="11" fill="currentColor" fillOpacity="0.12" stroke="currentColor" strokeWidth="2"/>
<circle cx="16" cy="16" r="4" fill="var(--accent, currentColor)" fillOpacity="0.3" stroke="var(--accent, currentColor)" strokeWidth="2"/>
<line x1="16" y1="4" x2="16" y2="8" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<line x1="16" y1="24" x2="16" y2="28" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<line x1="4" y1="16" x2="8" y2="16" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
<line x1="24" y1="16" x2="28" y2="16" stroke="var(--accent, currentColor)" strokeWidth="2" strokeLinecap="round"/>
</svg>
);
// Intelligent Svg Icon Resolver with Precision Word Boundaries
export function renderEnterpriseSvgIcon(identifier: string, size = 28, className?: string): React.ReactElement {
const key = identifier.toLowerCase();
// 1. TRUST, GUARANTEES & SOCIAL PROOF (Check first to avoid trade collisions)
if (key.includes('guarantee') || key.includes('warranty') || key.includes('satisfaction') || key.includes('peace of mind') || key.includes('licensed & insured')) {
return <RoyalShieldGuaranteeSvg size={size} className={className} />;
}
if (key.includes('review') || key.includes('5-star') || key.includes('five star') || key.includes('rated') || key.includes('testimonial') || key.includes('award')) {
return <FiveStarLaurelSvg size={size} className={className} />;
}
if (key.includes('dispatch') || key.includes('24/7') || key.includes('turnaround') || key.includes('response time') || key.includes('emergency') && (key.includes('dispatch') || key.includes('call'))) {
return <RapidDispatchSvg size={size} className={className} />;
}
if (key.includes('price') || key.includes('pricing') || key.includes('flat-rate') || key.includes('transparent') || key.includes('upfront') || key.includes('estimate') || key.includes('savings')) {
return <TransparentPricingSvg size={size} className={className} />;
}
// 2. POOL TRADE
if (key.includes('chem') || key.includes('balance') || key.includes('water test') || key.includes('chlorin') || key.includes('acid wash')) {
return <PoolChemistrySvg size={size} className={className} />;
}
if (key.includes('pump') || key.includes('motor') || key.includes('filter')) {
return <PoolPumpSvg size={size} className={className} />;
}
if (key.includes('tile') || key.includes('calcium') || key.includes('scrub') || key.includes('mineral')) {
return <SparklingTileSvg size={size} className={className} />;
}
if (key.includes('automation') || key.includes('screenlogic') || key.includes('salt') || key.includes('omnilogic')) {
return <SmartAutomationSvg size={size} className={className} />;
}
if (key.includes('green-to-clean') || key.includes('swamp') || key.includes('algae') || (key.includes('pool') && key.includes('clean'))) {
return <CrystalRescueSvg size={size} className={className} />;
}
// 3. LANDSCAPING TRADE
if (key.includes('paver') || key.includes('patio') || key.includes('hardscape') || key.includes('masonry') || key.includes('retaining') || key.includes('walkway')) {
return <PaverMasonrySvg size={size} className={className} />;
}
if (key.includes('lawn') || key.includes('mow') || key.includes('turf') || key.includes('grass') || key.includes('edging')) {
return <PrecisionLawnSvg size={size} className={className} />;
}
if (key.includes('sprinkler') || key.includes('irrigat') || key.includes('drip') || key.includes('valve repair')) {
return <IrrigationSpraySvg size={size} className={className} />;
}
if (key.includes('tree') || key.includes('prun') || key.includes('arborist') || key.includes('palm') || key.includes('branch')) {
return <TreeArboristSvg size={size} className={className} />;
}
if (key.includes('landscape lighting') || key.includes('luminary') || key.includes('low-voltage') || key.includes('outdoor light')) {
return <LandscapeLightingSvg size={size} className={className} />;
}
// 4. ROOFING TRADE
if (key.includes('shingle') || key.includes('roof') || key.includes('tpo') || key.includes('flat roof')) {
return <ArchitecturalRoofSvg size={size} className={className} />;
}
if (key.includes('tarp') || key.includes('storm damage') || (key.includes('roof') && key.includes('leak'))) {
return <StormShieldSvg size={size} className={className} />;
}
if (key.includes('gutter') || key.includes('downspout') || key.includes('seamless')) {
return <SeamlessGuttersSvg size={size} className={className} />;
}
// 5. PLUMBING TRADE
if (key.includes('tankless') || key.includes('water heater') || key.includes('hot water')) {
return <TanklessHeaterSvg size={size} className={className} />;
}
if (key.includes('hydro-jet') || key.includes('hydro jet') || key.includes('drain cleaning') || key.includes('rooter') || key.includes('sewer line')) {
return <HydroJetNozzleSvg size={size} className={className} />;
}
if (key.includes('pipe') || key.includes('valve') || key.includes('leak detection') || key.includes('repip') || key.includes('copper')) {
return <PipeValveSvg size={size} className={className} />;
}
// 6. HVAC TRADE
if (/\b(ac|a\/c|air condition|cooling|freon|condenser|chiller)\b/i.test(key)) {
return <AcCoolingSvg size={size} className={className} />;
}
if (key.includes('furnace') || key.includes('heating') || key.includes('burner') || key.includes('heat pump')) {
return <HeatingBurnerSvg size={size} className={className} />;
}
if (key.includes('thermostat') || key.includes('nest') || key.includes('ecobee') || key.includes('climate control')) {
return <SmartThermostatSvg size={size} className={className} />;
}
// 7. ELECTRICAL TRADE
if (key.includes('panel') || key.includes('breaker') || key.includes('busbar') || key.includes('200a') || key.includes('rewir')) {
return <ElectricPanelSvg size={size} className={className} />;
}
if (/\b(ev|charger|charging station|tesla)\b/i.test(key)) {
return <EvChargerSvg size={size} className={className} />;
}
if (key.includes('generator') || key.includes('standby power') || key.includes('backup power') || key.includes('turbine')) {
return <GeneratorTurbineSvg size={size} className={className} />;
}
// 8. MEDICAL / CLINIC / AESTHETIC
if (key.includes('laser') || key.includes('facial') || key.includes('skin') || key.includes('botox') || key.includes('aesthetic') || key.includes('dermal')) {
return <AestheticLaserSvg size={size} className={className} />;
}
// Fallback to Royal Shield Guarantee Svg
return <RoyalShieldGuaranteeSvg size={size} className={className} />;
}
+528
View File
@@ -0,0 +1,528 @@
import React, { useState, useEffect } from 'react';
import { BusinessProfile, ThemeId } from '../types';
import { THEMES, INDUSTRY_PRESETS, analyzeMissingProfileData } from '../services/aiEngine';
import {
Sparkles,
X,
Building2,
Phone,
Mail,
MapPin,
Wrench,
Plus,
Check,
Bot,
ArrowRight,
Zap,
Info,
Moon,
Sun
} from 'lucide-react';
interface OnboardingModalProps {
isOpen: boolean;
onClose: () => void;
profile: BusinessProfile;
onSaveProfile: (profile: BusinessProfile) => void;
}
export const OnboardingModal: React.FC<OnboardingModalProps> = ({
isOpen,
onClose,
profile,
onSaveProfile,
}) => {
const [formData, setFormData] = useState<BusinessProfile>({ ...profile, colorMode: profile.colorMode || 'dark' });
const [newServiceInput, setNewServiceInput] = useState('');
const [interviewMode, setInterviewMode] = useState(false);
const [aiChatLog, setAiChatLog] = useState<Array<{ sender: 'ai' | 'user'; text: string }>>([]);
const [aiResponseInput, setAiResponseInput] = useState('');
useEffect(() => {
if (isOpen) {
setFormData({ ...profile, colorMode: profile.colorMode || 'dark' });
}
}, [isOpen, profile]);
if (!isOpen) return null;
// Apply one-click preset
const handleApplyPreset = (presetKey: string) => {
const preset = INDUSTRY_PRESETS[presetKey];
if (!preset) return;
setFormData({
...formData,
companyName: preset.sampleCompany,
phone: preset.samplePhone,
address: preset.sampleAddress,
city: preset.sampleCity,
state: preset.sampleState,
zip: preset.sampleZip,
industry: preset.name,
tagline: preset.sampleTagline,
theme: preset.defaultTheme,
services: [...preset.defaultServices],
emergencyService: true,
yearsInBusiness: '12',
});
};
// Add service tag
const handleAddService = (e: React.FormEvent) => {
e.preventDefault();
if (!newServiceInput.trim()) return;
if (!formData.services.includes(newServiceInput.trim())) {
setFormData({
...formData,
services: [...formData.services, newServiceInput.trim()],
});
}
setNewServiceInput('');
};
const handleRemoveService = (serviceToRemove: string) => {
setFormData({
...formData,
services: formData.services.filter(s => s !== serviceToRemove),
});
};
// Check missing data and trigger AI Interviewer
const handleStartAiInterview = () => {
const analysis = analyzeMissingProfileData(formData);
setInterviewMode(true);
setAiChatLog([
{
sender: 'ai',
text: analysis.suggestedQuestion,
},
]);
};
const handleAiInterviewSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!aiResponseInput.trim()) return;
const userText = aiResponseInput.trim();
const updatedChat = [...aiChatLog, { sender: 'user' as const, text: userText }];
// Intelligent auto-filling from interview answer
const nextFormData = { ...formData };
if (!nextFormData.companyName) {
nextFormData.companyName = userText;
} else if (!nextFormData.phone) {
const phoneMatch = userText.match(/\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}/);
if (phoneMatch) nextFormData.phone = phoneMatch[0];
else nextFormData.phone = userText;
} else if (!nextFormData.city) {
nextFormData.city = userText;
} else if (nextFormData.services.length === 0) {
nextFormData.services = userText.split(',').map(s => s.trim()).filter(Boolean);
}
setFormData(nextFormData);
setAiResponseInput('');
// Re-check missing
const nextAnalysis = analyzeMissingProfileData(nextFormData);
if (nextAnalysis.isComplete) {
updatedChat.push({
sender: 'ai',
text: `🎉 Fantastic! I have gathered all key business details for **${nextFormData.companyName}**. Click **'Generate Website Now'** to render your interactive homepage draft!`,
});
} else {
updatedChat.push({
sender: 'ai',
text: nextAnalysis.suggestedQuestion,
});
}
setAiChatLog(updatedChat);
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
onSaveProfile(formData);
onClose();
};
return (
<div className="modal-overlay" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div className="modal-card">
{/* Modal Header */}
<div className="modal-header">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
<div className="brand-logo-icon" style={{ width: '34px', height: '34px' }}>
<Sparkles size={18} />
</div>
<div>
<h2 id="modal-title" className="modal-title">
{interviewMode ? 'AI Onboarding Interviewer' : 'Configure Your Business Website'}
</h2>
<p style={{ fontSize: '0.8rem', color: 'var(--text-secondary)' }}>
Step 1: Enter your business details or use the AI Assistant to formulate your brand
</p>
</div>
</div>
<button className="modal-close-btn" onClick={onClose} aria-label="Close modal">
<X size={20} />
</button>
</div>
{/* Modal Body */}
<div className="modal-body">
{/* Quick 1-Click Demo Presets */}
<div style={{ marginBottom: '1.5rem' }}>
<span style={{ fontSize: '0.78rem', fontWeight: 600, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: '0.05em' }}>
One-Click Industry Fast-Fill:
</span>
<div className="presets-strip" style={{ marginTop: '0.5rem' }}>
{Object.entries(INDUSTRY_PRESETS).map(([key, preset]) => (
<button
key={key}
type="button"
className="preset-card-chip"
onClick={() => handleApplyPreset(key)}
>
<Zap size={14} style={{ color: 'var(--accent)' }} />
<span style={{ fontSize: '0.82rem', fontWeight: 600, color: '#fff' }}>{preset.name}</span>
</button>
))}
</div>
</div>
{/* Toggle between Direct Form & AI Interviewer Mode */}
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '1.75rem' }}>
<button
type="button"
className={`btn ${!interviewMode ? 'btn-primary' : 'btn-secondary'}`}
style={{ flex: 1, padding: '0.65rem 1rem', fontSize: '0.85rem' }}
onClick={() => setInterviewMode(false)}
>
<Building2 size={16} />
<span>Standard Business Form</span>
</button>
<button
type="button"
className={`btn ${interviewMode ? 'btn-primary' : 'btn-secondary'}`}
style={{ flex: 1, padding: '0.65rem 1rem', fontSize: '0.85rem' }}
onClick={handleStartAiInterview}
>
<Bot size={16} />
<span>AI Conversational Interviewer</span>
</button>
</div>
{interviewMode ? (
/* AI INTERVIEW MODE */
<div>
<div style={{
background: 'rgba(0, 0, 0, 0.25)',
border: '1px solid var(--border-subtle)',
borderRadius: 'var(--radius-lg)',
padding: '1.25rem',
minHeight: '220px',
maxHeight: '320px',
overflowY: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '0.85rem',
marginBottom: '1rem'
}}>
{aiChatLog.map((chat, idx) => (
<div
key={idx}
className={`chat-bubble ${chat.sender}`}
style={{ maxWidth: '85%' }}
>
<div style={{ fontSize: '0.72rem', opacity: 0.7, marginBottom: '0.2rem' }}>
{chat.sender === 'ai' ? 'AI Website Architect' : 'You'}
</div>
<div>{chat.text}</div>
</div>
))}
</div>
<form onSubmit={handleAiInterviewSubmit} style={{ display: 'flex', gap: '0.5rem' }}>
<input
type="text"
className="form-input"
placeholder="Type your answer here..."
value={aiResponseInput}
onChange={(e) => setAiResponseInput(e.target.value)}
/>
<button type="submit" className="btn btn-primary" style={{ padding: '0 1.25rem' }}>
Send
</button>
</form>
</div>
) : (
/* STANDARD FORM MODE */
<form id="business-form" onSubmit={handleSubmit}>
<div className="form-grid">
{/* Company Name */}
<div>
<label className="form-label" htmlFor="company-name">Company Name *</label>
<input
id="company-name"
type="text"
className="form-input"
placeholder="e.g. Apex Pool Care"
value={formData.companyName}
onChange={(e) => setFormData({ ...formData, companyName: e.target.value })}
required
autoComplete="organization"
/>
</div>
{/* Phone Number */}
<div>
<label className="form-label" htmlFor="company-phone">Phone Number *</label>
<input
id="company-phone"
type="tel"
className="form-input"
placeholder="(555) 234-5678"
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
required
autoComplete="tel"
/>
</div>
{/* Primary Email */}
<div>
<label className="form-label" htmlFor="company-email">Primary Contact Email</label>
<input
id="company-email"
type="email"
className="form-input"
placeholder="contact@mycompany.com"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
autoComplete="email"
/>
</div>
{/* Industry */}
<div>
<label className="form-label" htmlFor="company-industry">Industry / Category</label>
<input
id="company-industry"
type="text"
className="form-input"
placeholder="e.g. Roofing & General Contracting"
value={formData.industry}
onChange={(e) => setFormData({ ...formData, industry: e.target.value })}
/>
</div>
{/* Address */}
<div className="form-group-full">
<label className="form-label" htmlFor="company-address">Street Address / Headquarters</label>
<input
id="company-address"
type="text"
className="form-input"
placeholder="1234 Main Street"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
autoComplete="street-address"
/>
</div>
{/* City, State, Zip */}
<div>
<label className="form-label" htmlFor="company-city">City / Service Area *</label>
<input
id="company-city"
type="text"
className="form-input"
placeholder="e.g. Dallas"
value={formData.city}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
required
autoComplete="address-level2"
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0.75rem' }}>
<div>
<label className="form-label" htmlFor="company-state">State</label>
<input
id="company-state"
type="text"
className="form-input"
placeholder="TX"
value={formData.state}
onChange={(e) => setFormData({ ...formData, state: e.target.value })}
autoComplete="address-level1"
/>
</div>
<div>
<label className="form-label" htmlFor="company-zip">Zip</label>
<input
id="company-zip"
type="text"
className="form-input"
placeholder="75001"
value={formData.zip}
onChange={(e) => setFormData({ ...formData, zip: e.target.value })}
autoComplete="postal-code"
/>
</div>
</div>
{/* Tagline / Value Proposition */}
<div className="form-group-full">
<label className="form-label" htmlFor="company-tagline">Tagline / Mission Statement</label>
<input
id="company-tagline"
type="text"
className="form-input"
placeholder="e.g. Master Craftsmanship, Fast Estimates & 100% Guaranteed Work"
value={formData.tagline}
onChange={(e) => setFormData({ ...formData, tagline: e.target.value })}
/>
</div>
{/* Core Services Tag List */}
<div className="form-group-full">
<label className="form-label">Core Services Offered ({formData.services.length})</label>
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.75rem' }}>
<input
type="text"
className="form-input"
placeholder="Type a service (e.g. Leak Detection) and press Add"
value={newServiceInput}
onChange={(e) => setNewServiceInput(e.target.value)}
/>
<button type="button" className="btn btn-secondary" onClick={handleAddService}>
<Plus size={16} />
<span>Add</span>
</button>
</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
{formData.services.map((svc, idx) => (
<span
key={idx}
style={{
display: 'inline-flex',
alignItems: 'center',
gap: '0.35rem',
background: 'rgba(56, 189, 248, 0.12)',
border: '1px solid var(--border-focus)',
color: 'var(--accent)',
padding: '4px 10px',
borderRadius: '9999px',
fontSize: '0.82rem',
fontWeight: 500,
}}
>
<Wrench size={12} />
<span>{svc}</span>
<button
type="button"
onClick={() => handleRemoveService(svc)}
style={{ background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer' }}
>
<X size={12} />
</button>
</span>
))}
</div>
</div>
{/* Emergency Service Toggle */}
<div className="form-group-full">
<label style={{ display: 'flex', alignItems: 'center', gap: '0.75rem', cursor: 'pointer', padding: '0.75rem 0' }}>
<input
type="checkbox"
checked={formData.emergencyService}
onChange={(e) => setFormData({ ...formData, emergencyService: e.target.checked })}
style={{ width: '18px', height: '18px', accentColor: 'var(--primary)' }}
/>
<span style={{ fontSize: '0.92rem', color: '#fff', fontWeight: 600 }}>
Provide 24/7 Emergency Dispatch / Rapid Response
</span>
</label>
</div>
{/* Appearance & Color Mode */}
<div className="form-group-full">
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '0.85rem', flexWrap: 'wrap', gap: '0.65rem' }}>
<div>
<label className="form-label" style={{ marginBottom: '2px' }}>Appearance & Color Mode</label>
<span style={{ fontSize: '0.78rem', color: 'var(--text-secondary)' }}>
Choose dark obsidian or crisp light styling for your site
</span>
</div>
<div className="color-mode-segmented">
<button
type="button"
className={`color-mode-toggle-btn ${(formData.colorMode || 'dark') === 'dark' ? 'active' : ''}`}
onClick={() => setFormData({ ...formData, colorMode: 'dark' })}
title="Dark Mode (Executive Obsidian & Vivid Glow)"
>
<Moon size={14} />
<span>Dark Mode</span>
</button>
<button
type="button"
className={`color-mode-toggle-btn ${formData.colorMode === 'light' ? 'active' : ''}`}
onClick={() => setFormData({ ...formData, colorMode: 'light' })}
title="Light Mode (Crisp High-Contrast & Slate)"
>
<Sun size={14} />
<span>Light Mode</span>
</button>
</div>
</div>
<label className="form-label" style={{ marginTop: '0.4rem' }}>Select Visual Theme</label>
<div className="theme-selector-grid">
{Object.values(THEMES).map((th) => (
<div
key={th.id}
className={`theme-swatch-card ${formData.theme === th.id ? 'active' : ''}`}
onClick={() => setFormData({ ...formData, theme: th.id })}
>
<div className="theme-swatch-colors">
<span className="swatch-circle" style={{ background: th.primary }}></span>
<span className="swatch-circle" style={{ background: th.accent }}></span>
</div>
<div style={{ fontSize: '0.85rem', fontWeight: 700, color: '#fff' }}>{th.name}</div>
<div style={{ fontSize: '0.7rem', color: 'var(--text-secondary)', marginTop: '2px' }}>{th.badge}</div>
</div>
))}
</div>
</div>
</div>
</form>
)}
</div>
{/* Modal Footer */}
<div className="modal-footer">
<button type="button" className="btn btn-secondary" onClick={onClose}>
Cancel
</button>
<button
type="button"
className="btn btn-primary"
onClick={() => {
onSaveProfile(formData);
onClose();
}}
>
<span>Generate Website Now</span>
<ArrowRight size={16} />
</button>
</div>
</div>
</div>
);
};
+477
View File
@@ -0,0 +1,477 @@
import React, { useState } from 'react';
import { WebsiteSection, BusinessProfile, SectionContent } from '../types';
import {
Sparkles,
Wrench,
Shield,
ShieldCheck,
Clock,
Star,
CheckCircle,
CheckCircle2,
Zap,
Tag,
Award,
Phone,
MapPin,
Calendar,
ArrowRight,
Edit3,
Flame,
Check,
Camera,
Image as ImageIcon
} from 'lucide-react';
import {
renderEnterpriseSvgIcon,
RoyalShieldGuaranteeSvg,
RapidDispatchSvg,
FiveStarLaurelSvg
} from './EnterpriseSvgIcons';
interface SectionRendererProps {
section: WebsiteSection;
profile: BusinessProfile;
onQuickAiEdit: (sectionNumber: number) => void;
onUpdateSectionContent?: (sectionNumber: number, newContent: Partial<SectionContent>) => void;
}
export const SectionRenderer: React.FC<SectionRendererProps> = ({
section,
profile,
onQuickAiEdit,
onUpdateSectionContent,
}) => {
const { content, sectionNumber } = section;
// Interactive Quote Form State
const [quoteSubmitted, setQuoteSubmitted] = useState(false);
const [leadName, setLeadName] = useState('');
const [leadPhone, setLeadPhone] = useState('');
const [leadService, setLeadService] = useState(profile.services[0] || 'General Consultation');
// Helper to render dynamic Lucide icon
const renderIcon = (iconName?: string) => {
switch (iconName) {
case 'wrench': return <Wrench size={24} />;
case 'shield-check': return <ShieldCheck size={24} />;
case 'shield': return <Shield size={24} />;
case 'clock': return <Clock size={24} />;
case 'star': return <Star size={24} />;
case 'check-circle': return <CheckCircle size={24} />;
case 'zap': return <Zap size={24} />;
case 'tag': return <Tag size={24} />;
case 'award': return <Award size={24} />;
case 'flame': return <Flame size={24} />;
default: return <Sparkles size={24} />;
}
};
return (
<div className="site-section-wrapper" id={`section-${sectionNumber}`}>
{/* Floating Section Indicator Badge */}
<div className="section-indicator-bar">
<span className="section-num-pill">SECTION {sectionNumber}</span>
<span className="section-indicator-label">{section.title}</span>
<span className="inline-edit-hint"> Click text to tweak directly</span>
<button
className="section-quick-edit-btn"
onClick={() => onQuickAiEdit(sectionNumber)}
title={`Tell AI: I don't like section ${sectionNumber}, make it like this...`}
>
<Edit3 size={13} />
<span>Edit with AI</span>
</button>
</div>
{/* SECTION 1: HERO BANNER */}
{section.type === 'hero' && (
<section className="hero-section hero-split-layout">
<div className="hero-content-col">
{content.trustBadges && content.trustBadges.length > 0 && (
<div className="hero-trust-pills">
{content.trustBadges.map((badge, idx) => (
<div key={idx} className="trust-pill">
<Check size={14} />
<span>{badge}</span>
</div>
))}
</div>
)}
<h1
className="hero-headline inline-editable-text"
contentEditable
suppressContentEditableWarning
title="Click to edit headline directly"
onBlur={(e) => onUpdateSectionContent?.(1, { headline: e.currentTarget.textContent || '' })}
>
{content.headline}
</h1>
<p
className="hero-subheadline inline-editable-text"
contentEditable
suppressContentEditableWarning
title="Click to edit subheadline directly"
onBlur={(e) => onUpdateSectionContent?.(1, { subheadline: e.currentTarget.textContent || '' })}
>
{content.subheadline}
</p>
<div className="hero-cta-group">
<a href="#quote" className="btn btn-primary" style={{ padding: '0.9rem 2rem', fontSize: '1.05rem' }}>
<span>{content.primaryCtaText || 'Request Free Estimate'}</span>
<ArrowRight size={18} />
</a>
<a href={`tel:${profile.phone}`} className="btn btn-secondary" style={{ padding: '0.9rem 1.75rem', fontSize: '1.05rem' }}>
<Phone size={18} />
<span>{content.secondaryCtaText || `Call: ${profile.phone}`}</span>
</a>
</div>
<div className="hero-guarantee-row">
<div style={{ display: 'flex', alignItems: 'center', gap: '0.45rem' }}>
<RoyalShieldGuaranteeSvg size={19} />
<span>100% Satisfaction Guarantee</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.45rem' }}>
<RapidDispatchSvg size={19} />
<span>Direct Phone Support</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.45rem' }}>
<FiveStarLaurelSvg size={19} />
<span>Top Rated Local Service</span>
</div>
</div>
</div>
<div className="hero-media-col">
{content.heroImageUrl ? (
<div className="hero-image-card">
<img
src={`${content.heroImageUrl}?v=real_v2`}
alt={content.headline || 'Service Technician'}
className="hero-showcase-img"
loading="eager"
/>
<div className="hero-image-overlay-badge">
<div className="overlay-badge-pulse"></div>
<span>Verified Master Tech On-Duty</span>
</div>
<div className="hero-image-stat-card">
<Star size={16} fill="#fbbf24" stroke="#fbbf24" />
<div>
<strong style={{ display: 'block', color: '#fff', fontSize: '0.85rem' }}>5.0 Star Rated</strong>
<span style={{ fontSize: '0.72rem', color: 'var(--text-secondary)' }}>Local Emergency Dispatch</span>
</div>
</div>
</div>
) : (
<div className="hero-image-placeholder-card" onClick={() => onQuickAiEdit?.(1)}>
<div className="placeholder-icon-ring">
<Camera size={30} style={{ color: 'var(--accent)' }} />
</div>
<h4 className="placeholder-card-title">Commercial Photography Spotlight</h4>
<p className="placeholder-card-desc">Showcase spot for certified technicians, equipment, and 5.0-star local verification.</p>
<div className="placeholder-action-pill">
<Sparkles size={14} style={{ color: 'var(--accent)' }} />
<span>Click to Place Trade Image</span>
</div>
</div>
)}
</div>
</section>
)}
{/* SECTION 2: SERVICES GRID */}
{section.type === 'services' && (
<section className="services-section" id="services">
<div className="section-header">
<span className="section-eyebrow">Capabilities & Offerings</span>
<h2 className="section-title">{content.headline || 'Professional Services'}</h2>
<p className="section-description">{content.subheadline}</p>
</div>
<div className={`services-grid ${content.columns === 4 ? 'cols-4' : ''}`}>
{content.servicesList?.map((service, idx) => (
<div key={service.id || idx} className="service-card">
{service.popular && (
<span className="service-badge-popular">Top Requested</span>
)}
{service.imageUrl && (
<div className="service-thumbnail-wrap">
<img
src={`${service.imageUrl}?v=real_v2`}
alt={service.title}
className="service-thumbnail-img"
loading="lazy"
/>
</div>
)}
<div className="service-icon-box">
{renderEnterpriseSvgIcon(service.title || service.iconName || 'service', 28)}
</div>
<h3 className="service-card-title">{service.title}</h3>
<p className="service-card-desc">{service.description}</p>
<a href="#quote" className="service-link">
<span>Schedule Service</span>
<ArrowRight size={14} />
</a>
</div>
))}
</div>
</section>
)}
{/* SECTION 3: WHY CHOOSE US */}
{section.type === 'why-us' && (
<section className="why-us-section" id="why-us">
<div className="section-header">
<span className="section-eyebrow">Proven Reliability</span>
<h2 className="section-title">{content.headline || 'Why Choose Our Team'}</h2>
<p className="section-description">{content.subheadline}</p>
</div>
{content.stats && content.stats.length > 0 && (
<div className="stats-grid">
{content.stats.map((st, idx) => (
<div key={st.id || idx} className="stat-card">
<div className="stat-value">{st.value}</div>
<div className="stat-label">{st.label}</div>
{st.subtext && <div className="stat-subtext">{st.subtext}</div>}
</div>
))}
</div>
)}
{content.pillars && content.pillars.length > 0 && (
<div className="pillars-grid">
{content.pillars.map((pil, idx) => (
<div key={pil.id || idx} className="pillar-item">
<div className="pillar-icon-box">
{renderEnterpriseSvgIcon(pil.title || pil.iconName || 'pillar', 26)}
</div>
<div>
<h3 className="pillar-title">{pil.title}</h3>
<p className="pillar-desc">{pil.description}</p>
</div>
</div>
))}
</div>
)}
</section>
)}
{/* SECTION 4: TESTIMONIALS */}
{section.type === 'testimonials' && (
<section className="testimonials-section" id="reviews">
<div className="section-header">
<span className="section-eyebrow">Verified Reviews</span>
<h2 className="section-title">{content.headline || 'What Customers Are Saying'}</h2>
<p className="section-description">{content.subheadline}</p>
</div>
<div className="reviews-grid">
{content.reviews?.map((rev, idx) => (
<div key={rev.id || idx} className="review-card">
<div>
<div className="review-stars">
{[...Array(rev.rating || 5)].map((_, i) => (
<Star key={i} size={18} fill="#fbbf24" stroke="#fbbf24" />
))}
</div>
<p className="review-quote">"{rev.quote}"</p>
</div>
<div className="review-author-row">
<div className="author-avatar">
{rev.author.charAt(0)}
</div>
<div>
<div className="author-name">{rev.author}</div>
<div className="author-location">
<MapPin size={12} style={{ display: 'inline', marginRight: '3px' }} />
{rev.location} {rev.serviceMentioned ? `${rev.serviceMentioned}` : ''}
</div>
</div>
</div>
</div>
))}
</div>
</section>
)}
{/* SECTION 5: QUOTE FORM */}
{section.type === 'quote-form' && (
<section className="quote-section" id="quote">
<div className="quote-box">
<div className="section-header" style={{ marginBottom: '2rem' }}>
<span className="section-eyebrow">Fast Response</span>
<h2 className="section-title" style={{ fontSize: '2rem' }}>{content.formTitle || 'Request an Estimate'}</h2>
<p className="section-description">{content.formSubtitle}</p>
</div>
{quoteSubmitted ? (
<div className="quote-success-card">
<div className="quote-success-icon-ring">
<CheckCircle2 size={44} style={{ color: '#34d399' }} />
</div>
<h3 className="quote-success-title">Estimate Request Confirmed!</h3>
<p className="quote-success-desc">
Thank you, <strong>{leadName || 'Valued Customer'}</strong>! Your inquiry for <em>{leadService || 'Priority Service'}</em> has been dispatched directly to <strong>{profile.companyName}</strong>.
</p>
<div className="quote-success-pill">
<Clock size={16} style={{ color: 'var(--accent)' }} />
<span>Guaranteed response in <strong>&lt; 15 minutes</strong> at <strong>{leadPhone || profile.phone}</strong></span>
</div>
<button
type="button"
className="btn btn-secondary"
style={{ marginTop: '1.25rem', padding: '0.65rem 1.4rem', fontSize: '0.85rem' }}
onClick={() => setQuoteSubmitted(false)}
>
Submit Another Test Request
</button>
</div>
) : (
<form onSubmit={(e) => {
e.preventDefault();
setQuoteSubmitted(true);
}}>
<div className="form-grid">
<div>
<label className="form-label">Full Name *</label>
<input
type="text"
className="form-input"
placeholder="e.g. Michael Henderson"
value={leadName}
onChange={(e) => setLeadName(e.target.value)}
required
/>
</div>
<div>
<label className="form-label">Phone Number *</label>
<input
type="tel"
className="form-input"
placeholder="(555) 000-0000"
value={leadPhone}
onChange={(e) => setLeadPhone(e.target.value)}
required
/>
</div>
<div className="form-group-full">
<label className="form-label">Email Address</label>
<input type="email" className="form-input" placeholder="michael@example.com" />
</div>
<div className="form-group-full">
<label className="form-label">Service Required</label>
<select
className="form-select"
value={leadService}
onChange={(e) => setLeadService(e.target.value)}
>
{profile.services.length > 0 ? (
profile.services.map((s, idx) => (
<option key={idx} value={s}>{s}</option>
))
) : (
<option value="general">General Estimate & Consultation</option>
)}
</select>
</div>
<div className="form-group-full">
<label className="form-label">Project Details / Message</label>
<textarea className="form-textarea" placeholder="Describe your property needs, timeline, or address..."></textarea>
</div>
<div className="form-group-full" style={{ marginTop: '0.5rem' }}>
<button type="submit" className="btn btn-primary" style={{ width: '100%', padding: '1rem' }}>
<span>Submit Free Quote Request</span>
<ArrowRight size={18} />
</button>
</div>
</div>
</form>
)}
</div>
</section>
)}
{/* SECTION 6: CTA BANNER */}
{section.type === 'cta-banner' && (
<section className="cta-banner-section">
<div className="cta-banner-box">
<h2 className="cta-banner-title">{content.bannerTitle}</h2>
<p className="cta-banner-desc">{content.bannerDescription}</p>
<a href={`tel:${profile.phone}`} className="btn btn-accent" style={{ padding: '0.9rem 2.2rem', fontSize: '1.05rem' }}>
<Phone size={18} />
<span>{content.bannerCta || `Call: ${profile.phone}`}</span>
</a>
</div>
</section>
)}
{/* SECTION 7: FOOTER */}
{section.type === 'footer' && (
<footer className="footer-section">
<div className="footer-grid">
<div>
<div className="site-logo">
<div className="brand-logo-icon">
{renderEnterpriseSvgIcon(profile.industry || profile.companyName || 'brand', 22)}
</div>
<span>{profile.companyName}</span>
</div>
<p className="footer-company-desc">
{profile.tagline}. Dedicated to providing premier services throughout {profile.city || 'the region'} with licensed technicians and ironclad warranties.
</p>
<div style={{ marginTop: '1.25rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', fontSize: '0.9rem' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: '#fff' }}>
<Phone size={16} style={{ color: 'var(--accent)' }} />
<span>{profile.phone}</span>
</div>
{profile.address && (
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--text-secondary)' }}>
<MapPin size={16} style={{ color: 'var(--accent)' }} />
<span>{profile.address}, {profile.city}, {profile.state} {profile.zip}</span>
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', color: 'var(--text-secondary)' }}>
<Calendar size={16} style={{ color: 'var(--accent)' }} />
<span>{content.hours || 'Mon-Sat: 7:00 AM - 6:30 PM'}</span>
</div>
</div>
</div>
<div>
<h4 className="footer-col-title">Navigation</h4>
<ul className="footer-links-list">
{content.links?.map((lnk, idx) => (
<li key={idx}><a href={lnk.href}>{lnk.label}</a></li>
))}
</ul>
</div>
<div>
<h4 className="footer-col-title">Services Offered</h4>
<ul className="footer-links-list">
{profile.services.slice(0, 5).map((s, idx) => (
<li key={idx}><a href="#quote">{s}</a></li>
))}
</ul>
</div>
</div>
<div className="footer-bottom-bar">
<div>{content.copyrightText}</div>
<div className="whitelabel-credit">
<span>Site Architecture by</span>
<strong style={{ color: '#fff' }}>AI Pilots Site Engine</strong>
</div>
</div>
</footer>
)}
</div>
);
};
+269
View File
@@ -0,0 +1,269 @@
import React, { useState } from 'react';
import { WebsiteSection, BusinessProfile } from '../types';
import {
Sparkles,
Phone,
ArrowRight,
Menu,
X,
ShieldCheck,
Clock,
Star,
Award,
Edit3,
MapPin,
CheckCircle
} from 'lucide-react';
import { renderEnterpriseSvgIcon } from './EnterpriseSvgIcons';
interface SiteHeaderProps {
section: WebsiteSection;
profile: BusinessProfile;
viewportMode: 'desktop' | 'tablet' | 'mobile';
onQuickAiEdit: (sectionNumber: number) => void;
}
export const SiteHeader: React.FC<SiteHeaderProps> = ({
section,
profile,
viewportMode,
onQuickAiEdit,
}) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const { content, sectionNumber } = section;
const variant = content.headerVariant || 'emergency-trade';
const showTopBar = content.showTopBar !== false;
const ctaText = content.headerCtaText || (variant === 'emergency-trade' ? 'Emergency Dispatch' : 'Get Free Quote');
const navLinks = content.navLinks && content.navLinks.length > 0
? content.navLinks
: [
{ label: 'Services', href: '#services' },
{ label: 'Why Us', href: '#why-us' },
{ label: 'Reviews', href: '#reviews' },
{ label: 'Free Estimate', href: '#quote' },
];
const cityState = profile.city && profile.state
? `${profile.city}, ${profile.state}`
: (profile.city || 'Greater Area');
const defaultTopAnnouncement = variant === 'modern-clean'
? 'Rated 5.0 Stars by Over 1,200+ Satisfied Homeowners • 100% Satisfaction Guarantee'
: variant === 'commercial-elite'
? 'Certified Master Commercial & Residential Contractors • Licensed & Bonded'
: '24/7 Rapid Emergency Response Available • Guaranteed On-Time Arrival';
const announcement = content.topBarAnnouncement || defaultTopAnnouncement;
const licenseBadge = content.licenseBadge || 'CA Lic #948201 • Fully Insured';
const serviceArea = content.serviceAreaText || `Serving ${cityState} & Nearby`;
const handleNavClick = (href: string) => {
setMobileMenuOpen(false);
const targetEl = document.querySelector(href);
if (targetEl) {
targetEl.scrollIntoView({ behavior: 'smooth' });
}
};
return (
<div className="site-section-wrapper header-section-wrapper" id="section-0">
{/* Floating Section Indicator Badge */}
<div className="section-indicator-bar">
<span className="section-num-pill">SECTION 0</span>
<span className="section-indicator-label">Header & Navigation</span>
<button
className="section-quick-edit-btn"
onClick={() => onQuickAiEdit(sectionNumber)}
title="Tell AI to modify Section 0 (Header)"
>
<Edit3 size={13} />
<span>AI Edit</span>
</button>
</div>
<header className={`preformatted-site-header variant-${variant}`}>
{/* 1. TOP EMERGENCY & TRUST BAR */}
{showTopBar && (
<div className="site-topbar">
<div className="topbar-inner">
{/* Left announcement / live pulse */}
<div className="topbar-left">
{variant === 'emergency-trade' ? (
<div className="topbar-pulse-item">
<span className="pulse-dot pulse-dot-green" />
<span className="topbar-text font-semibold">{announcement}</span>
</div>
) : variant === 'modern-clean' ? (
<div className="topbar-pulse-item">
<Star size={13} style={{ color: '#eab308' }} fill="#eab308" />
<span className="topbar-text">{announcement}</span>
</div>
) : (
<div className="topbar-pulse-item">
<Award size={13} style={{ color: 'var(--accent)' }} />
<span className="topbar-text">{announcement}</span>
</div>
)}
</div>
{/* Right trust badges (desktop & tablet) */}
<div className="topbar-right">
<span className="topbar-badge">
<ShieldCheck size={13} />
<span>{licenseBadge}</span>
</span>
<span className="topbar-separator"></span>
<span className="topbar-badge topbar-area">
<MapPin size={13} />
<span>{serviceArea}</span>
</span>
</div>
</div>
</div>
)}
{/* 2. MAIN NAVIGATION BAR */}
<div className="site-main-nav">
<div className="main-nav-inner">
{/* Brand Logo & Tagline */}
<div className="site-logo-wrap">
<a href="#section-0" className="site-logo" onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }); }}>
<div className="brand-logo-icon">
{renderEnterpriseSvgIcon(profile.industry || profile.companyName || 'brand', 22)}
</div>
<div className="logo-text-block">
<span className="logo-company-name">{profile.companyName || 'Apex Business'}</span>
<span className="logo-subtext">{profile.industry || 'Certified Master Pros'}</span>
</div>
</a>
</div>
{/* Desktop Navigation Links */}
<nav className="desktop-nav-menu" aria-label="Main Navigation">
<ul className="site-nav-links">
{navLinks.map((link, idx) => (
<li key={idx}>
<a href={link.href} onClick={(e) => { e.preventDefault(); handleNavClick(link.href); }}>
{link.label}
</a>
</li>
))}
</ul>
</nav>
{/* Action CTAs */}
<div className="site-nav-cta-cluster">
{/* Phone CTA */}
{profile.phone && (
<a href={`tel:${profile.phone}`} className="nav-phone-button" title={`Call ${profile.phone}`}>
<div className="phone-icon-circle">
<Phone size={15} />
</div>
<div className="phone-text-block">
<span className="phone-sub-label">Speak With Tech</span>
<span className="phone-number">{profile.phone}</span>
</div>
</a>
)}
{/* Primary Quote CTA Button */}
<a
href="#quote"
className="btn btn-primary nav-primary-cta"
onClick={(e) => { e.preventDefault(); handleNavClick('#quote'); }}
>
<span>{ctaText}</span>
<ArrowRight size={14} />
</a>
{/* Mobile / iPad Hamburger Menu Button */}
<button
type="button"
className="mobile-hamburger-btn"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label={mobileMenuOpen ? 'Close Menu' : 'Open Menu'}
aria-expanded={mobileMenuOpen}
>
{mobileMenuOpen ? <X size={20} /> : <Menu size={20} />}
</button>
</div>
</div>
</div>
{/* 3. INTERACTIVE SLIDE-DOWN MOBILE & TABLET MENU DRAWER */}
{mobileMenuOpen && (
<div className="mobile-nav-drawer-overlay">
<div className="mobile-nav-drawer">
<div className="mobile-drawer-header">
<div className="drawer-brand">
<Sparkles size={18} style={{ color: 'var(--accent)' }} />
<span>{profile.companyName || 'Apex Business'}</span>
</div>
<button
type="button"
className="drawer-close-btn"
onClick={() => setMobileMenuOpen(false)}
>
<X size={18} />
</button>
</div>
{/* Mobile Navigation Links */}
<ul className="mobile-nav-links">
{navLinks.map((link, idx) => (
<li key={idx}>
<a
href={link.href}
onClick={(e) => { e.preventDefault(); handleNavClick(link.href); }}
>
<span>{link.label}</span>
<ArrowRight size={15} style={{ opacity: 0.5 }} />
</a>
</li>
))}
</ul>
{/* Mobile Click-to-Call Direct Banner */}
{profile.phone && (
<a href={`tel:${profile.phone}`} className="mobile-drawer-call-card">
<div className="drawer-call-icon">
<Phone size={18} />
</div>
<div>
<div className="drawer-call-title">Call For Immediate Service</div>
<div className="drawer-call-number">{profile.phone}</div>
</div>
<span className="pulse-dot pulse-dot-green ml-auto" />
</a>
)}
{/* Mobile Full-Width Quote Button */}
<a
href="#quote"
className="btn btn-primary mobile-drawer-cta"
onClick={(e) => { e.preventDefault(); handleNavClick('#quote'); }}
>
<span>{ctaText}</span>
<ArrowRight size={16} />
</a>
{/* Mobile Trust Footer */}
<div className="mobile-drawer-footer">
<div className="drawer-trust-item">
<CheckCircle size={14} style={{ color: '#22c55e' }} />
<span>{licenseBadge}</span>
</div>
<div className="drawer-trust-item">
<Clock size={14} style={{ color: 'var(--accent)' }} />
<span>24/7 Dispatch Free Estimates</span>
</div>
</div>
</div>
</div>
)}
</header>
</div>
);
};
+91
View File
@@ -0,0 +1,91 @@
import React from 'react';
import { WebsiteSection, BusinessProfile, SectionContent } from '../types';
import { SectionRenderer } from './SectionRenderer';
import { SiteHeader } from './SiteHeader';
import { Phone, Zap } from 'lucide-react';
interface WebsitePreviewProps {
sections: WebsiteSection[];
profile: BusinessProfile;
viewportMode: 'desktop' | 'tablet' | 'mobile';
onQuickAiEdit: (sectionNumber: number) => void;
onUpdateSectionContent?: (sectionNumber: number, newContent: Partial<SectionContent>) => void;
}
export const WebsitePreview: React.FC<WebsitePreviewProps> = ({
sections,
profile,
viewportMode,
onQuickAiEdit,
onUpdateSectionContent,
}) => {
// Find Section 0 (Header) or fallback to default
const headerSection: WebsiteSection = sections.find(
(s) => s.type === 'header' || s.sectionNumber === 0
) || {
id: 'sec-header',
sectionNumber: 0,
type: 'header',
title: 'Header & Navigation',
badge: 'Standard Header',
visible: true,
content: {
headerVariant: 'emergency-trade',
showTopBar: true,
headerCtaText: 'Emergency Dispatch',
},
};
// Main sections (excluding Section 0)
const mainSections = sections.filter(
(s) => s.type !== 'header' && s.sectionNumber !== 0
);
return (
<div className={`site-frame-container viewport-${viewportMode}`}>
{viewportMode === 'mobile' && <div className="mobile-notch" />}
<div className="site-canvas">
{/* Preformatted Responsive Header & Navigation (Section 0) */}
{headerSection.visible && (
<SiteHeader
section={headerSection}
profile={profile}
viewportMode={viewportMode}
onQuickAiEdit={onQuickAiEdit}
/>
)}
{/* Dynamic Sections Stack */}
<main>
{mainSections.map((section) => (
section.visible && (
<SectionRenderer
key={section.id}
section={section}
profile={profile}
onQuickAiEdit={onQuickAiEdit}
onUpdateSectionContent={onUpdateSectionContent}
/>
)
))}
</main>
{/* Sticky Mobile Lead & Call Action Bar */}
{viewportMode === 'mobile' && (
<div className="mobile-sticky-action-bar">
<a href={`tel:${profile.phone}`} className="mobile-sticky-call-btn">
<span className="mobile-call-pulse-dot" />
<Phone size={15} />
<span>Call Now</span>
</a>
<a href="#quote" className="mobile-sticky-quote-btn">
<Zap size={14} />
<span>Free Estimate</span>
</a>
</div>
)}
</div>
</div>
);
};
+3254
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './index.css';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
File diff suppressed because it is too large Load Diff
+180
View File
@@ -0,0 +1,180 @@
export type ThemeId =
| 'titan-navy'
| 'obsidian-gold'
| 'emerald-slate'
| 'crimson-forge'
| 'alpine-clean'
| 'electric-violet'
| 'sunset-amber'
| 'cyber-cyan'
| 'neon-rose'
| 'custom-dynamic';
export type ColorMode = 'dark' | 'light';
export interface ThemeConfig {
id: ThemeId;
name: string;
tagline: string;
primary: string;
secondary: string;
accent: string;
bgDark: string;
bgLight: string;
cardBgDark: string;
cardBgLight: string;
borderDark: string;
borderLight: string;
}
export interface BusinessProfile {
companyName: string;
phone: string;
email: string;
address: string;
city: string;
state: string;
zip: string;
industry: string;
tagline: string;
yearsInBusiness: string;
services: string[];
emergencyService: boolean;
theme: ThemeId;
colorMode?: ColorMode;
}
export type SectionType =
| 'header'
| 'hero'
| 'services'
| 'why-us'
| 'testimonials'
| 'quote-form'
| 'cta-banner'
| 'footer';
export interface ServiceItem {
id: string;
title: string;
description: string;
iconName: string;
popular?: boolean;
imageUrl?: string;
}
export interface TestimonialItem {
id: string;
author: string;
location: string;
rating: number;
quote: string;
serviceMentioned?: string;
date?: string;
}
export interface StatItem {
id: string;
value: string;
label: string;
subtext?: string;
}
export interface ValuePillarItem {
id: string;
title: string;
description: string;
iconName: string;
}
export interface SectionContent {
// Header section content
headerVariant?: 'emergency-trade' | 'modern-clean' | 'commercial-elite';
showTopBar?: boolean;
topBarAnnouncement?: string;
licenseBadge?: string;
serviceAreaText?: string;
headerCtaText?: string;
navLinks?: Array<{ label: string; href: string }>;
// Hero section content
headline?: string;
subheadline?: string;
primaryCtaText?: string;
secondaryCtaText?: string;
trustBadges?: string[];
heroImageUrl?: string;
// Services section content
servicesList?: ServiceItem[];
columns?: number;
// Why Us content
stats?: StatItem[];
pillars?: ValuePillarItem[];
// Testimonials content
reviews?: TestimonialItem[];
averageRating?: number;
totalReviews?: number;
// Quote form content
formTitle?: string;
formSubtitle?: string;
fields?: Array<{ label: string; type: string; placeholder: string; required?: boolean }>;
// CTA Banner content
bannerTitle?: string;
bannerDescription?: string;
bannerCta?: string;
// Footer content
hours?: string;
copyrightText?: string;
links?: Array<{ label: string; href: string }>;
}
export interface WebsiteSection {
id: string;
sectionNumber: number;
type: SectionType;
title: string;
badge?: string;
content: SectionContent;
visible: boolean;
}
export interface ChatMessage {
id: string;
sender: 'user' | 'ai' | 'system';
text: string;
timestamp: string;
actionSuggested?: {
type: 'apply_change' | 'select_theme' | 'fill_field' | 'generate_site';
payload?: any;
label?: string;
};
modifiedSectionNumber?: number;
}
export interface ClientContactDetails {
name: string;
email: string;
phone: string;
domainPreference?: string;
notes?: string;
wantsGmb: boolean;
wantsMailcowEmail: boolean;
wantsCrm: boolean;
}
export interface LeadSubmission {
id: string;
createdAt: string;
businessProfile: BusinessProfile;
clientContact: ClientContactDetails;
sections: WebsiteSection[];
theme: ThemeId;
colorMode?: ColorMode;
status: 'pending_review' | 'contacted' | 'deployed';
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}
+1
View File
@@ -0,0 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/types.ts","./src/components/agencyadmindrawer.tsx","./src/components/aiassistantdrawer.tsx","./src/components/approvalmodal.tsx","./src/components/enterprisesvgicons.tsx","./src/components/onboardingmodal.tsx","./src/components/sectionrenderer.tsx","./src/components/siteheader.tsx","./src/components/websitepreview.tsx","./src/services/aiengine.ts"],"version":"5.9.3"}
+11
View File
@@ -0,0 +1,11 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
host: true
}
});