Removed stray documentation from repo and added the /demo endpoint
All checks were successful
ci/woodpecker/manual/woodpecker Pipeline was successful

This commit is contained in:
Josh 2025-10-17 08:58:23 +00:00
parent e6f98f9653
commit 6a58f62075
11 changed files with 356 additions and 17 deletions

View File

@ -1 +1 @@
ec484f55e8a48bee6d0e6223d06b815335ea5e74-006fe5ea7fd8cb46290bcd0cf210f88f2ec04061-e9eccd451b778829eb2f2c9752c670b707e1268b 7525e7b74f06b3341cb73a157afaea13b4af1f5d-006fe5ea7fd8cb46290bcd0cf210f88f2ec04061-e9eccd451b778829eb2f2c9752c670b707e1268b

View File

@ -37,3 +37,13 @@ test-results/
blob-report/ blob-report/
*.trace.zip *.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

10
.gitignore vendored
View File

@ -28,3 +28,13 @@ uploads/.env
scan-env.sh scan-env.sh
.aptiva-test-user.json .aptiva-test-user.json
APTIVA_AI_FEATURES_DOCUMENTATION.md 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

View File

@ -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: `<pre style="font-family: ui-monospace, Menlo, monospace; white-space: pre-wrap">${emailBody}</pre>`,
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) ----------------- */ /* ----------------- Support bot chat (server2) ----------------- */
/* CREATE thread */ /* CREATE thread */

View File

@ -5043,7 +5043,7 @@ app.post(
userPlan = 'premium'; userPlan = 'premium';
} }
const weeklyLimits = { premium: 3, pro: 5 }; const weeklyLimits = { premium: 10, pro: 10 };
const userWeeklyLimit = weeklyLimits[userPlan] || 0; const userWeeklyLimit = weeklyLimits[userPlan] || 0;
let resetDate = new Date(userProfile.resume_limit_reset); let resetDate = new Date(userProfile.resume_limit_reset);
@ -5143,7 +5143,7 @@ app.get('/api/premium/resume/remaining', authenticatePremiumUser, async (req, re
userPlan = 'premium'; userPlan = 'premium';
} }
const weeklyLimits = { basic: 1, premium: 2, pro: 5 }; const weeklyLimits = { basic: 0, premium: 10, pro: 10 };
const userWeeklyLimit = weeklyLimits[userPlan] || 0; const userWeeklyLimit = weeklyLimits[userPlan] || 0;
let resetDate = new Date(userProfile.resume_limit_reset); let resetDate = new Date(userProfile.resume_limit_reset);

View File

@ -266,3 +266,16 @@ ALTER TABLE career_profiles
ADD COLUMN resume_filesize INT UNSIGNED NULL AFTER resume_filename, ADD COLUMN resume_filesize INT UNSIGNED NULL AFTER resume_filename,
ADD COLUMN resume_uploaded_at DATETIME NULL AFTER resume_filesize; 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;

View File

@ -191,10 +191,14 @@ http {
proxy_set_header Connection ""; proxy_set_header Connection "";
} }
location = /api/user-profile { limit_conn perip 5; location = /api/user-profile { limit_conn perip 5;
limit_req zone=reqperip burst=10 nodelay; limit_req zone=reqperip burst=10 nodelay;
proxy_pass http://backend5000; } 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 # General API (anything not matched above) rate-limited
location ^~ /api/ { proxy_pass http://backend5000; } location ^~ /api/ { proxy_pass http://backend5000; }

View File

@ -51,6 +51,7 @@ import { initNetObserver } from './utils/net.js';
import PrivacyPolicy from './components/PrivacyPolicy.js'; import PrivacyPolicy from './components/PrivacyPolicy.js';
import TermsOfService from './components/TermsOfService.js'; import TermsOfService from './components/TermsOfService.js';
import HomePage from './components/HomePage.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 === '/forgot-password' ||
location.pathname === '/privacy' || location.pathname === '/privacy' ||
location.pathname === '/terms' || location.pathname === '/terms' ||
location.pathname === '/home' location.pathname === '/home' ||
location.pathname === '/demo'
) { ) {
try { localStorage.removeItem('id'); } catch {} try { localStorage.removeItem('id'); } catch {}
setIsAuthenticated(false); setIsAuthenticated(false);
@ -269,7 +271,8 @@ if (loggingOut) return;
p === '/paywall' || p === '/paywall' ||
p === '/privacy' || p === '/privacy' ||
p === '/terms' || p === '/terms' ||
p === '/home'; p === '/home' ||
p === '/demo';
if (!onPublic) navigate('/signin?session=expired', { replace: true }); if (!onPublic) navigate('/signin?session=expired', { replace: true });
} finally { } finally {
if (!cancelled) setIsLoading(false); if (!cancelled) setIsLoading(false);
@ -810,6 +813,9 @@ const cancelLogout = () => {
{/* Public Home Page */} {/* Public Home Page */}
<Route path="/home" element={<HomePage />} /> <Route path="/home" element={<HomePage />} />
{/* Public Demo Request (Conference Lead Capture) */}
<Route path="/demo" element={<DemoRequest />} />
{/* Default */} {/* Default */}
<Route <Route
path="/" path="/"

View File

@ -0,0 +1,216 @@
import { useState } from 'react';
import axios from 'axios';
const DemoRequest = () => {
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 (
<div className="min-h-screen flex items-center justify-center p-5 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500">
<div className="bg-white rounded-xl shadow-2xl max-w-md w-full p-12 text-center">
<div className="w-20 h-20 bg-gradient-to-br from-green-500 to-green-600 text-white text-5xl rounded-full flex items-center justify-center mx-auto mb-6 animate-[scaleIn_0.5s_ease-out]">
</div>
<h2 className="text-2xl font-semibold text-gray-900 mb-4">Request Received!</h2>
<p className="text-base text-gray-600 leading-relaxed mb-8">
We've received your demo request and will reach out soon to schedule a time that works for you.
</p>
<div className="pt-6 border-t border-gray-200">
<p className="text-sm text-gray-500 mb-3">Want to connect sooner?</p>
<a
href={`mailto:jcoakley@aptivaai.com?subject=Demo Request: ${formData.organization_name}`}
className="inline-block bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-6 py-3 rounded-lg font-semibold text-sm transition-all hover:-translate-y-0.5 hover:shadow-[0_6px_20px_rgba(102,126,234,0.4)]"
>
Email jcoakley@aptivaai.com directly
</a>
</div>
</div>
</div>
);
}
return (
<div className="min-h-screen flex items-center justify-center p-5 bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500">
<div className="bg-white rounded-xl shadow-2xl max-w-lg w-full p-10">
<div className="text-center mb-8">
<h1 className="text-3xl font-semibold text-gray-900 mb-2">Schedule a Demo</h1>
<p className="text-base text-gray-500">Career Planning for the AI Era</p>
</div>
<form onSubmit={handleSubmit} className="flex flex-col gap-5">
<div className="flex flex-col">
<label htmlFor="name" className="text-sm font-medium text-gray-700 mb-1.5">
Name <span className="text-red-600">*</span>
</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Your full name"
disabled={loading}
autoComplete="name"
className="px-3.5 py-3 text-base border-2 border-gray-200 rounded-lg transition-all focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 disabled:bg-gray-50 disabled:cursor-not-allowed"
/>
</div>
<div className="flex flex-col">
<label htmlFor="email" className="text-sm font-medium text-gray-700 mb-1.5">
Email <span className="text-red-600">*</span>
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="you@organization.edu"
disabled={loading}
autoComplete="email"
className="px-3.5 py-3 text-base border-2 border-gray-200 rounded-lg transition-all focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 disabled:bg-gray-50 disabled:cursor-not-allowed"
/>
</div>
<div className="flex flex-col">
<label htmlFor="organization_name" className="text-sm font-medium text-gray-700 mb-1.5">
Organization <span className="text-red-600">*</span>
</label>
<input
type="text"
id="organization_name"
name="organization_name"
value={formData.organization_name}
onChange={handleChange}
placeholder="School, district, or college name"
disabled={loading}
autoComplete="organization"
className="px-3.5 py-3 text-base border-2 border-gray-200 rounded-lg transition-all focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 disabled:bg-gray-50 disabled:cursor-not-allowed"
/>
</div>
<div className="flex flex-col">
<label htmlFor="phone" className="text-sm font-medium text-gray-700 mb-1.5">
Phone (optional)
</label>
<input
type="tel"
id="phone"
name="phone"
value={formData.phone}
onChange={handleChange}
placeholder="(555) 123-4567"
disabled={loading}
autoComplete="tel"
className="px-3.5 py-3 text-base border-2 border-gray-200 rounded-lg transition-all focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 disabled:bg-gray-50 disabled:cursor-not-allowed"
/>
</div>
<div className="flex flex-col">
<label htmlFor="message" className="text-sm font-medium text-gray-700 mb-1.5">
Message (optional)
</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
rows="3"
disabled={loading}
placeholder="Tell us about your needs..."
className="px-3.5 py-3 text-base border-2 border-gray-200 rounded-lg transition-all focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-100 disabled:bg-gray-50 disabled:cursor-not-allowed resize-y min-h-[80px]"
/>
</div>
{error && (
<div className="bg-red-50 text-red-700 px-4 py-3 rounded-lg border-l-4 border-red-500 text-sm">
{error}
</div>
)}
<button
type="submit"
className="bg-gradient-to-r from-indigo-500 to-purple-600 text-white px-6 py-3.5 text-base font-semibold rounded-lg transition-all mt-2.5 hover:-translate-y-0.5 hover:shadow-[0_6px_20px_rgba(102,126,234,0.4)] active:translate-y-0 disabled:opacity-60 disabled:cursor-not-allowed"
disabled={loading}
>
{loading ? 'Submitting...' : 'Request Demo'}
</button>
</form>
<div className="mt-5 text-center">
<p className="text-xs text-gray-500 leading-relaxed">
We respect your privacy. Your information will only be used to contact you about Aptiva AI.
</p>
</div>
</div>
</div>
);
};
export default DemoRequest;

View File

@ -38,7 +38,7 @@ export default function HomePage() {
</Button> </Button>
</div> </div>
<p className="text-sm text-gray-500 mt-4"> <p className="text-sm text-gray-500 mt-4">
Free forever · No credit card required · Full access to career planning tools No credit card required · Full access to career planning tools
</p> </p>
</div> </div>
</section> </section>
@ -201,7 +201,7 @@ export default function HomePage() {
<svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /> <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg> </svg>
<span>AI Career Coach powered by GPT-4o for personalized milestone recommendations</span> <span>AI Career Coach Agent for personalized milestone recommendations and automated milestone/task/impact creation</span>
</li> </li>
<li className="flex items-start"> <li className="flex items-start">
<svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
@ -213,7 +213,7 @@ export default function HomePage() {
<svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /> <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg> </svg>
<span>Resume optimizer tailored to specific job descriptions (2-5/week)</span> <span>Resume optimizer tailored to specific job descriptions (10/week)</span>
</li> </li>
</ul> </ul>
</div> </div>
@ -311,13 +311,13 @@ export default function HomePage() {
<div className="text-center"> <div className="text-center">
<div className="bg-aptiva/10 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4"> <div className="bg-aptiva/10 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-aptiva" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg className="w-8 h-8 text-aptiva" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg> </svg>
</div> </div>
<h3 className="text-xl font-bold text-gray-900 mb-2">Affordable</h3> <h3 className="text-xl font-bold text-gray-900 mb-2">Privacy-First</h3>
<p className="text-gray-600"> <p className="text-gray-600">
Premium features start at just $4.99/monthless than a coffee per week. We never ask for your birthdate or age. Plan your career and retirement
Free tier includes comprehensive career exploration tools. without giving up personal data. Your privacy is protected.
</p> </p>
</div> </div>
</div> </div>
@ -439,7 +439,7 @@ export default function HomePage() {
<svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" /> <path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
</svg> </svg>
<span>AI Resume optimizer (2 per week)</span> <span>AI Resume optimizer (10 per week)</span>
</li> </li>
<li className="flex items-start"> <li className="flex items-start">
<svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"> <svg className="w-5 h-5 text-green-500 mr-2 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">

View File

@ -118,7 +118,7 @@ export default function Paywall() {
</li> </li>
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<span className="text-green-600 font-bold mt-0.5"></span> <span className="text-green-600 font-bold mt-0.5"></span>
<span><strong>Resume Optimizer</strong> 3 AI-enhanced optimizations per week</span> <span><strong>Resume Optimizer</strong> 10 AI-enhanced optimizations per week</span>
</li> </li>
<li className="flex items-start gap-2"> <li className="flex items-start gap-2">
<span className="text-green-600 font-bold mt-0.5"></span> <span className="text-green-600 font-bold mt-0.5"></span>