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:
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