diff --git a/.build.hash b/.build.hash index ec7a2d8..0ce7a74 100644 --- a/.build.hash +++ b/.build.hash @@ -1 +1 @@ -ec484f55e8a48bee6d0e6223d06b815335ea5e74-006fe5ea7fd8cb46290bcd0cf210f88f2ec04061-e9eccd451b778829eb2f2c9752c670b707e1268b +7525e7b74f06b3341cb73a157afaea13b4af1f5d-006fe5ea7fd8cb46290bcd0cf210f88f2ec04061-e9eccd451b778829eb2f2c9752c670b707e1268b diff --git a/.dockerignore b/.dockerignore index 2e2c4da..1e8fb70 100644 --- a/.dockerignore +++ b/.dockerignore @@ -37,3 +37,13 @@ test-results/ blob-report/ *.trace.zip +# Conference and business planning documents (not needed in containers) +COMPETITIVE_ANALYSIS.md +PRICING_OPERATIONS_ANALYSIS.md +INFRASTRUCTURE_SCALING_ANALYSIS.md +COST_PROJECTION_DATA_NEEDED.md +ACCURATE_COST_PROJECTIONS.md +GAETC_PRINT_MATERIALS_FINAL.md +CONFERENCE_MATERIALS.md +APTIVA_AI_FEATURES_DOCUMENTATION.md + diff --git a/.gitignore b/.gitignore index b371a05..63c9f93 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,13 @@ uploads/.env scan-env.sh .aptiva-test-user.json APTIVA_AI_FEATURES_DOCUMENTATION.md + +# Conference and business planning documents (sensitive) +COMPETITIVE_ANALYSIS.md +PRICING_OPERATIONS_ANALYSIS.md +INFRASTRUCTURE_SCALING_ANALYSIS.md +COST_PROJECTION_DATA_NEEDED.md +ACCURATE_COST_PROJECTIONS.md +GAETC_PRINT_MATERIALS_FINAL.md +CONFERENCE_MATERIALS.md +APTIVA_AI_FEATURES_DOCUMENTATION.md diff --git a/backend/server2.js b/backend/server2.js index 2a2e412..432c9e0 100755 --- a/backend/server2.js +++ b/backend/server2.js @@ -1734,6 +1734,86 @@ ${body}`; } ); +/* ----------------- Demo Request (Conference Lead Capture) ----------------- */ + +app.post('/api/demo-request', async (req, res) => { + try { + const { name, email, organization_name, phone, message } = req.body; + + // Validation + if (!name || !email || !organization_name) { + return res.status(400).json({ + error: 'Name, email, and organization name are required' + }); + } + + // Email validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return res.status(400).json({ error: 'Invalid email format' }); + } + + // Insert to database (fields will be encrypted via withEncryption wrapper) + const query = ` + INSERT INTO demo_requests + (name, email, organization_name, phone, message, source) + VALUES (?, ?, ?, ?, ?, ?) + `; + + const source = 'gaetc_nov2025'; // TODO: Update this before each conference + + await pool.execute(query, [ + name.trim(), + email.trim().toLowerCase(), + organization_name.trim(), + phone?.trim() || null, + message?.trim() || null, + source + ]); + + // Send email notification + if (SENDGRID_KEY) { + try { + const emailBody = `New demo request received: + +Name: ${name} +Email: ${email} +Organization: ${organization_name} +Phone: ${phone || 'Not provided'} +Message: ${message || 'None'} + +Source: ${source} +Submitted: ${new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })} + +--- +Reply directly to ${email} to follow up.`; + + await sgMail.send({ + to: 'jcoakley@aptivaai.com', + from: 'noreply@aptivaai.com', + replyTo: email, + subject: `New Demo Request: ${organization_name}`, + text: emailBody, + html: `
${emailBody}
`, + categories: ['demo-request', source] + }); + } catch (emailErr) { + console.error('[demo-request] Email notification failed:', emailErr?.message || emailErr); + // Don't fail the request if email fails - data is still saved + } + } + + res.json({ + success: true, + message: 'Demo request received! We\'ll reach out soon to schedule a time that works for you.' + }); + + } catch (error) { + console.error('[demo-request] Error:', error?.message || error); + res.status(500).json({ error: 'Failed to submit request. Please try again.' }); + } +}); + /* ----------------- Support bot chat (server2) ----------------- */ /* CREATE thread */ diff --git a/backend/server3.js b/backend/server3.js index 051435d..9871849 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -5043,7 +5043,7 @@ app.post( userPlan = 'premium'; } - const weeklyLimits = { premium: 3, pro: 5 }; + const weeklyLimits = { premium: 10, pro: 10 }; const userWeeklyLimit = weeklyLimits[userPlan] || 0; let resetDate = new Date(userProfile.resume_limit_reset); @@ -5143,7 +5143,7 @@ app.get('/api/premium/resume/remaining', authenticatePremiumUser, async (req, re userPlan = 'premium'; } - const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; + const weeklyLimits = { basic: 0, premium: 10, pro: 10 }; const userWeeklyLimit = weeklyLimits[userPlan] || 0; let resetDate = new Date(userProfile.resume_limit_reset); diff --git a/migrate_encrypted_columns.sql b/migrate_encrypted_columns.sql index 60f72d6..11f82cb 100644 --- a/migrate_encrypted_columns.sql +++ b/migrate_encrypted_columns.sql @@ -266,3 +266,16 @@ ALTER TABLE career_profiles ADD COLUMN resume_filesize INT UNSIGNED NULL AFTER resume_filename, ADD COLUMN resume_uploaded_at DATETIME NULL AFTER resume_filesize; + +CREATE TABLE IF NOT EXISTS demo_requests ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(512) NOT NULL, + email VARCHAR(512) NOT NULL, + organization_name VARCHAR(512) NOT NULL, + phone VARCHAR(512), + message TEXT, + source VARCHAR(512) DEFAULT 'conference', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_created (created_at), + INDEX idx_source (source(255)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/nginx.conf b/nginx.conf index 9ee05a9..87aa974 100644 --- a/nginx.conf +++ b/nginx.conf @@ -191,10 +191,14 @@ http { proxy_set_header Connection ""; } - location = /api/user-profile { limit_conn perip 5; - limit_req zone=reqperip burst=10 nodelay; + location = /api/user-profile { limit_conn perip 5; + limit_req zone=reqperip burst=10 nodelay; proxy_pass http://backend5000; } - + + location = /api/demo-request { limit_conn perip 5; + limit_req zone=reqperip burst=10 nodelay; + proxy_pass http://backend5001; } + # General API (anything not matched above) – rate-limited location ^~ /api/ { proxy_pass http://backend5000; } diff --git a/src/App.js b/src/App.js index 8d7c7b7..8db0d31 100644 --- a/src/App.js +++ b/src/App.js @@ -51,6 +51,7 @@ import { initNetObserver } from './utils/net.js'; import PrivacyPolicy from './components/PrivacyPolicy.js'; import TermsOfService from './components/TermsOfService.js'; import HomePage from './components/HomePage.js'; +import DemoRequest from './components/DemoRequest.js'; @@ -231,7 +232,8 @@ if (loggingOut) return; location.pathname === '/forgot-password' || location.pathname === '/privacy' || location.pathname === '/terms' || - location.pathname === '/home' + location.pathname === '/home' || + location.pathname === '/demo' ) { try { localStorage.removeItem('id'); } catch {} setIsAuthenticated(false); @@ -269,7 +271,8 @@ if (loggingOut) return; p === '/paywall' || p === '/privacy' || p === '/terms' || - p === '/home'; + p === '/home' || + p === '/demo'; if (!onPublic) navigate('/signin?session=expired', { replace: true }); } finally { if (!cancelled) setIsLoading(false); @@ -810,6 +813,9 @@ const cancelLogout = () => { {/* Public Home Page */} } /> + {/* Public Demo Request (Conference Lead Capture) */} + } /> + {/* Default */} { + const [formData, setFormData] = useState({ + name: '', + email: '', + organization_name: '', + phone: '', + message: "I'd like to schedule a demo" + }); + const [submitted, setSubmitted] = useState(false); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + + const handleChange = (e) => { + const { name, value } = e.target; + setFormData(prev => ({ + ...prev, + [name]: value + })); + // Clear error when user starts typing + if (error) setError(''); + }; + + const validateForm = () => { + if (!formData.name.trim()) { + setError('Please enter your name'); + return false; + } + if (!formData.email.trim()) { + setError('Please enter your email'); + return false; + } + // Email format validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + setError('Please enter a valid email address'); + return false; + } + if (!formData.organization_name.trim()) { + setError('Please enter your organization name'); + return false; + } + return true; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (!validateForm()) return; + + setLoading(true); + setError(''); + + try { + const response = await axios.post('/api/demo-request', formData); + + if (response.data.success) { + setSubmitted(true); + } + } catch (err) { + console.error('Demo request error:', err); + setError( + err.response?.data?.error || + 'Failed to submit request. Please try again or email us directly.' + ); + } finally { + setLoading(false); + } + }; + + if (submitted) { + return ( +
+
+
+ ✓ +
+

Request Received!

+

+ We've received your demo request and will reach out soon to schedule a time that works for you. +

+
+

Want to connect sooner?

+ + Email jcoakley@aptivaai.com directly + +
+
+
+ ); + } + + return ( +
+
+
+

Schedule a Demo

+

Career Planning for the AI Era

+
+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +