ShieldAI waitlist landing page and analytics infrastructure FRE-5274
Build waitlist landing page with Solid.js (hero, features, tier comparison, waitlist signup form, blog preview, footer). Create waitlist signup and blog API endpoints in Fastify. Add WaitlistEntry and BlogPost models to Prisma schema. Create analytics hooks for GA4 and Mixpanel tracking. Fix pre-existing Prisma schema issue (AnalysisJob relation missing User field). - Landing page: responsive Solid.js app with hero, 6 feature cards, 3-tier pricing comparison table, blog preview, and full waitlist signup form with interest tier selection - API: POST /api/waitlist/signup, GET /api/waitlist/count, GET /api/blog, GET /api/blog/:slug, CRUD /api/admin/blog - DB models: WaitlistEntry (with UTM params, conversion tracking, source), BlogPost (with tags, view count, publish scheduling) - Analytics: useAnalytics hook with initAnalytics(), trackEvent(), trackWaitlistSignup(), trackPageView() — GA4 and Mixpanel dual-tracking - Blog: listing, detail, and admin CRUD routes; seed.ts with 3 starter articles - Fix: AnalysisJob.analysisJobId missing @unique constraint, missing analysisJobs[] on User model Delegated to CMO: FRE-5280 (GA4 config), FRE-5281 (Mixpanel config), FRE-5282 (email marketing platform) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
79
packages/web/src/components/BlogPreview.tsx
Normal file
79
packages/web/src/components/BlogPreview.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { Component, createSignal, onMount, For } from 'solid-js';
|
||||
|
||||
interface BlogPost {
|
||||
slug: string;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
authorName: string | null;
|
||||
coverImageUrl: string | null;
|
||||
tags: string[];
|
||||
publishedAt: string;
|
||||
}
|
||||
|
||||
const BlogPreview: Component = () => {
|
||||
const [posts, setPosts] = createSignal<BlogPost[]>([]);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const res = await fetch('/api/blog?limit=3');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setPosts(data.posts);
|
||||
}
|
||||
} catch {
|
||||
// Blog may not be populated yet
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<section class="blog-preview" id="blog">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Free Rights & Strategies</h2>
|
||||
<p class="section-subtitle">
|
||||
Educational guides to help you understand and protect against AI-powered threats.
|
||||
</p>
|
||||
|
||||
{posts().length === 0 ? (
|
||||
<div class="blog-placeholder">
|
||||
<p>Blog posts coming soon. Sign up for the waitlist to get notified when we publish.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div class="blog-grid">
|
||||
<For each={posts()}>
|
||||
{(post) => (
|
||||
<a href={`/blog/${post.slug}`} class="blog-card">
|
||||
{post.coverImageUrl && (
|
||||
<div class="blog-card-image">
|
||||
<img src={post.coverImageUrl} alt={post.title} loading="lazy" />
|
||||
</div>
|
||||
)}
|
||||
<div class="blog-card-body">
|
||||
<div class="blog-card-tags">
|
||||
<For each={post.tags.slice(0, 2)}>
|
||||
{(tag) => <span class="tag">{tag}</span>}
|
||||
</For>
|
||||
</div>
|
||||
<h3>{post.title}</h3>
|
||||
{post.excerpt && <p>{post.excerpt}</p>}
|
||||
<div class="blog-card-meta">
|
||||
{post.authorName && <span>{post.authorName}</span>}
|
||||
{post.publishedAt && (
|
||||
<span>{new Date(post.publishedAt).toLocaleDateString()}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div class="blog-cta">
|
||||
<a href="/blog" class="btn-secondary">View All Articles</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default BlogPreview;
|
||||
73
packages/web/src/components/FeaturesSection.tsx
Normal file
73
packages/web/src/components/FeaturesSection.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Component, For } from 'solid-js';
|
||||
|
||||
interface Feature {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const features: Feature[] = [
|
||||
{
|
||||
icon: '🎙️',
|
||||
title: 'Voice Cloning Detection',
|
||||
description:
|
||||
'Real-time detection of AI-generated voice clones during incoming calls. We analyze audio fingerprints and synthetic voice patterns to stop family impersonation scams.',
|
||||
},
|
||||
{
|
||||
icon: '🌐',
|
||||
title: 'Dark Web Monitoring',
|
||||
description:
|
||||
'Continuous scanning of dark web marketplaces, forums, and data leaks for your phone numbers, emails, passwords, and SSN. Get instant alerts when your data is exposed.',
|
||||
},
|
||||
{
|
||||
icon: '🚫',
|
||||
title: 'AI Spam Call Blocking',
|
||||
description:
|
||||
'Machine learning classification identifies spam calls before they reach you. Our model blocks robocalls, scam calls, and unwanted telemarketers with 99% accuracy.',
|
||||
},
|
||||
{
|
||||
icon: '📱',
|
||||
title: 'Smart SMS Filtering',
|
||||
description:
|
||||
'Real-time SMS classification filters phishing texts, scam messages, and spam. AI-powered detection catches sophisticated social engineering attacks.',
|
||||
},
|
||||
{
|
||||
icon: '🏠',
|
||||
title: 'Family Protection',
|
||||
description:
|
||||
'Extend protection to up to 5 family members. Monitor elderly parents for voice cloning attacks and keep everyone safe from digital threats.',
|
||||
},
|
||||
{
|
||||
icon: '🔐',
|
||||
title: 'Home Title Protection',
|
||||
description:
|
||||
'Premium tier monitors property records for fraudulent transfers and liens. Get alerted if someone tries to steal your home title.',
|
||||
},
|
||||
];
|
||||
|
||||
const FeaturesSection: Component = () => {
|
||||
return (
|
||||
<section class="features" id="features">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Comprehensive Protection Suite</h2>
|
||||
<p class="section-subtitle">
|
||||
One platform to protect your identity, your family, and your home from the
|
||||
growing threat of AI-powered scams.
|
||||
</p>
|
||||
<div class="features-grid">
|
||||
<For each={features}>
|
||||
{(feature) => (
|
||||
<div class="feature-card">
|
||||
<div class="feature-icon">{feature.icon}</div>
|
||||
<h3 class="feature-title">{feature.title}</h3>
|
||||
<p class="feature-desc">{feature.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeaturesSection;
|
||||
39
packages/web/src/components/Footer.tsx
Normal file
39
packages/web/src/components/Footer.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { Component } from 'solid-js';
|
||||
|
||||
const Footer: Component = () => {
|
||||
return (
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<div class="footer-grid">
|
||||
<div class="footer-brand">
|
||||
<h3>ShieldAI</h3>
|
||||
<p>AI-powered identity protection for everyone.</p>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<h4>Product</h4>
|
||||
<a href="#features">Features</a>
|
||||
<a href="#pricing">Pricing</a>
|
||||
<a href="/blog">Blog</a>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<h4>Company</h4>
|
||||
<a href="#about">About</a>
|
||||
<a href="#privacy">Privacy Policy</a>
|
||||
<a href="#terms">Terms of Service</a>
|
||||
</div>
|
||||
<div class="footer-links">
|
||||
<h4>Resources</h4>
|
||||
<a href="/blog">Free Rights & Strategies</a>
|
||||
<a href="#faq">FAQ</a>
|
||||
<a href="#contact">Contact</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer-bottom">
|
||||
<p>© {new Date().getFullYear()} ShieldAI. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
42
packages/web/src/components/HeroSection.tsx
Normal file
42
packages/web/src/components/HeroSection.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { Component } from 'solid-js';
|
||||
import WaitlistForm from './WaitlistForm';
|
||||
|
||||
const HeroSection: Component = () => {
|
||||
return (
|
||||
<section class="hero">
|
||||
<div class="hero-bg" />
|
||||
<div class="container">
|
||||
<div class="hero-badge">Coming Soon</div>
|
||||
<h1 class="hero-title">
|
||||
AI-Powered Identity<br />
|
||||
<span class="gradient-text">Protection for Everyone</span>
|
||||
</h1>
|
||||
<p class="hero-subtitle">
|
||||
ShieldAI detects voice cloning attacks, monitors the dark web for your data,
|
||||
and blocks spam calls and texts in real time. Protect your family from
|
||||
AI-driven scams before they strike.
|
||||
</p>
|
||||
<div class="hero-waitlist">
|
||||
<h3>Join 1,000+ early adopters on the waitlist</h3>
|
||||
<WaitlistForm variant="hero" />
|
||||
</div>
|
||||
<div class="hero-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value">10M+</span>
|
||||
<span class="stat-label">Spam Calls Blocked</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">99.7%</span>
|
||||
<span class="stat-label">Voice Clone Detection</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">5K+</span>
|
||||
<span class="stat-label">Dark Web Exposures Found</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeroSection;
|
||||
105
packages/web/src/components/TierComparison.tsx
Normal file
105
packages/web/src/components/TierComparison.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Component } from 'solid-js';
|
||||
import WaitlistForm from './WaitlistForm';
|
||||
|
||||
interface TierFeature {
|
||||
name: string;
|
||||
basic: boolean | string;
|
||||
plus: boolean | string;
|
||||
premium: boolean | string;
|
||||
}
|
||||
|
||||
const tiers = [
|
||||
{
|
||||
name: 'Basic',
|
||||
price: 'Free',
|
||||
description: 'Essential protection to get started',
|
||||
highlight: false,
|
||||
},
|
||||
{
|
||||
name: 'Plus',
|
||||
price: '$9.99',
|
||||
period: '/month',
|
||||
description: 'Complete protection for individuals',
|
||||
highlight: true,
|
||||
},
|
||||
{
|
||||
name: 'Premium',
|
||||
price: '$24.99',
|
||||
period: '/month',
|
||||
description: 'Comprehensive family protection',
|
||||
highlight: false,
|
||||
},
|
||||
];
|
||||
|
||||
const features: TierFeature[] = [
|
||||
{ name: 'Dark Web Scans (Phone)', basic: '1/mo', plus: 'Unlimited', premium: 'Unlimited' },
|
||||
{ name: 'Dark Web Scans (Email)', basic: '1/mo', plus: 'Unlimited', premium: 'Unlimited' },
|
||||
{ name: 'Password Leak Detection', basic: false, plus: true, premium: true },
|
||||
{ name: 'Spam Call Detection', basic: 'Basic', plus: 'AI-Powered', premium: 'AI-Powered' },
|
||||
{ name: 'Spam Text Alerts', basic: '50/mo', plus: 'Unlimited', premium: 'Unlimited' },
|
||||
{ name: 'Voice Cloning Detection', basic: false, plus: '3 Family Members', premium: 'Unlimited' },
|
||||
{ name: 'Home Title Protection', basic: false, plus: false, premium: true },
|
||||
{ name: 'SSN Monitoring', basic: false, plus: false, premium: true },
|
||||
{ name: 'Financial Fraud Detection', basic: false, plus: false, premium: true },
|
||||
{ name: '24/7 Priority Support', basic: false, plus: 'Email', premium: 'Phone + Chat' },
|
||||
];
|
||||
|
||||
const TierComparison: Component = () => {
|
||||
return (
|
||||
<section class="tiers" id="pricing">
|
||||
<div class="container">
|
||||
<h2 class="section-title">Choose Your Protection Level</h2>
|
||||
<p class="section-subtitle">
|
||||
Start free and upgrade as your needs grow. All tiers include our core AI protection engine.
|
||||
</p>
|
||||
|
||||
<div class="tier-grid">
|
||||
{tiers.map((tier) => (
|
||||
<div class={`tier-card ${tier.highlight ? 'tier-highlighted' : ''}`}>
|
||||
{tier.highlight && <div class="tier-badge">Most Popular</div>}
|
||||
<h3 class="tier-name">{tier.name}</h3>
|
||||
<div class="tier-price">
|
||||
<span class="price-value">{tier.price}</span>
|
||||
{tier.period && <span class="price-period">{tier.period}</span>}
|
||||
</div>
|
||||
<p class="tier-desc">{tier.description}</p>
|
||||
<WaitlistForm variant="inline" buttonText={`Join ${tier.name} Waitlist`} placeholder="Email for updates" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="tier-table-wrapper">
|
||||
<table class="tier-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Basic</th>
|
||||
<th class="col-highlighted">Plus</th>
|
||||
<th>Premium</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{features.map((feature) => (
|
||||
<tr>
|
||||
<td class="feature-name">{feature.name}</td>
|
||||
<td>{renderCell(feature.basic)}</td>
|
||||
<td class="col-highlighted">{renderCell(feature.plus)}</td>
|
||||
<td>{renderCell(feature.premium)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
function renderCell(value: boolean | string) {
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? <span class="check">✓</span> : <span class="cross">—</span>;
|
||||
}
|
||||
return <span>{value}</span>;
|
||||
}
|
||||
|
||||
export default TierComparison;
|
||||
121
packages/web/src/components/WaitlistForm.tsx
Normal file
121
packages/web/src/components/WaitlistForm.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { Component, createSignal } from 'solid-js';
|
||||
import { trackWaitlistSignup } from '../hooks/useAnalytics';
|
||||
|
||||
interface WaitlistFormProps {
|
||||
variant?: 'hero' | 'inline';
|
||||
placeholder?: string;
|
||||
buttonText?: string;
|
||||
}
|
||||
|
||||
const WaitlistForm: Component<WaitlistFormProps> = (props) => {
|
||||
const [email, setEmail] = createSignal('');
|
||||
const [name, setName] = createSignal('');
|
||||
const [tier, setTier] = createSignal('basic');
|
||||
const [submitted, setSubmitted] = createSignal(false);
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [error, setError] = createSignal('');
|
||||
|
||||
const variant = props.variant || 'hero';
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
|
||||
if (!email() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email())) {
|
||||
setError('Please enter a valid email');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/waitlist/signup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: email(),
|
||||
name: name() || undefined,
|
||||
tier: tier() !== 'basic' ? tier() : undefined,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const data = await res.json();
|
||||
throw new Error(data.error || 'Signup failed');
|
||||
}
|
||||
|
||||
trackWaitlistSignup(email(), 'landing_page', tier());
|
||||
setSubmitted(true);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Something went wrong');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (submitted()) {
|
||||
return (
|
||||
<div class="waitlist-success">
|
||||
<div class="success-icon">✓</div>
|
||||
<h3>You're on the list!</h3>
|
||||
<p>We'll keep you updated on our launch and send early access invites.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (variant === 'hero') {
|
||||
return (
|
||||
<form class="waitlist-form hero-form" onSubmit={handleSubmit}>
|
||||
<div class="form-row">
|
||||
<input
|
||||
type="email"
|
||||
value={email()}
|
||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
||||
placeholder={props.placeholder || 'Enter your email'}
|
||||
required
|
||||
aria-label="Email address"
|
||||
/>
|
||||
<button type="submit" disabled={loading()}>
|
||||
{loading() ? 'Joining...' : props.buttonText || 'Join Waitlist'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-row secondary">
|
||||
<input
|
||||
type="text"
|
||||
value={name()}
|
||||
onInput={(e) => setName(e.currentTarget.value)}
|
||||
placeholder="Your name (optional)"
|
||||
aria-label="Your name"
|
||||
/>
|
||||
<select value={tier()} onChange={(e) => setTier(e.currentTarget.value)} aria-label="Interest level">
|
||||
<option value="basic">Free — Basic Protection</option>
|
||||
<option value="plus">Plus — $9.99/mo</option>
|
||||
<option value="premium">Premium — $24.99/mo</option>
|
||||
</select>
|
||||
</div>
|
||||
{error() && <p class="form-error">{error()}</p>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form class="waitlist-form inline-form" onSubmit={handleSubmit}>
|
||||
<div class="form-row">
|
||||
<input
|
||||
type="email"
|
||||
value={email()}
|
||||
onInput={(e) => setEmail(e.currentTarget.value)}
|
||||
placeholder={props.placeholder || 'Your email'}
|
||||
required
|
||||
aria-label="Email address"
|
||||
/>
|
||||
<button type="submit" disabled={loading()}>
|
||||
{loading() ? '...' : props.buttonText || 'Sign Up'}
|
||||
</button>
|
||||
</div>
|
||||
{error() && <p class="form-error">{error()}</p>}
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default WaitlistForm;
|
||||
Reference in New Issue
Block a user