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:
2026-05-13 23:47:25 -04:00
parent 65c7da4852
commit 9d4865306c
28 changed files with 2311 additions and 1 deletions

936
packages/web/src/index.css Normal file
View File

@@ -0,0 +1,936 @@
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--bg-primary: #0a0f1e;
--bg-secondary: #111827;
--bg-card: #1a2332;
--bg-card-hover: #1f2b3d;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent-primary: #3b82f6;
--accent-secondary: #06b6d4;
--accent-gradient: linear-gradient(135deg, #3b82f6, #06b6d4);
--border-color: #1e293b;
--border-light: #334155;
--success: #22c55e;
--error: #ef4444;
--radius: 12px;
--radius-sm: 8px;
--max-width: 1200px;
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
}
html {
scroll-behavior: smooth;
font-size: 16px;
}
body {
font-family: var(--font-sans);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
a {
color: var(--accent-primary);
text-decoration: none;
}
a:hover {
color: var(--accent-secondary);
}
img {
max-width: 100%;
height: auto;
}
.container {
max-width: var(--max-width);
margin: 0 auto;
padding: 0 24px;
}
.section-title {
font-size: 2.5rem;
font-weight: 800;
text-align: center;
margin-bottom: 16px;
line-height: 1.2;
}
.section-subtitle {
font-size: 1.125rem;
color: var(--text-secondary);
text-align: center;
max-width: 640px;
margin: 0 auto 64px;
line-height: 1.7;
}
.gradient-text {
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* Hero Section */
.hero {
position: relative;
min-height: 100vh;
display: flex;
align-items: center;
overflow: hidden;
padding: 120px 0 80px;
}
.hero-bg {
position: absolute;
inset: 0;
background:
radial-gradient(ellipse 80% 60% at 50% -20%, rgba(59, 130, 246, 0.15), transparent),
radial-gradient(ellipse 50% 40% at 80% 80%, rgba(6, 182, 212, 0.1), transparent),
radial-gradient(ellipse 40% 30% at 20% 70%, rgba(99, 102, 241, 0.08), transparent);
pointer-events: none;
}
.hero-badge {
display: inline-block;
padding: 6px 16px;
border-radius: 999px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
color: var(--accent-primary);
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 24px;
}
.hero-title {
font-size: 4rem;
font-weight: 800;
line-height: 1.1;
margin-bottom: 24px;
letter-spacing: -0.02em;
}
.hero-subtitle {
font-size: 1.25rem;
color: var(--text-secondary);
max-width: 600px;
margin-bottom: 48px;
line-height: 1.7;
}
.hero-waitlist h3 {
font-size: 1rem;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 16px;
}
.hero-stats {
display: flex;
gap: 48px;
margin-top: 80px;
padding-top: 48px;
border-top: 1px solid var(--border-color);
}
.stat {
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 2rem;
font-weight: 800;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.stat-label {
font-size: 0.875rem;
color: var(--text-muted);
margin-top: 4px;
}
/* Waitlist Form */
.waitlist-form {
max-width: 560px;
}
.form-row {
display: flex;
gap: 12px;
margin-bottom: 12px;
}
.form-row.secondary {
gap: 12px;
}
.form-row.secondary input,
.form-row.secondary select {
flex: 1;
}
.waitlist-form input[type="email"],
.waitlist-form input[type="text"],
.waitlist-form select {
flex: 1;
padding: 14px 18px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-light);
background: var(--bg-card);
color: var(--text-primary);
font-size: 1rem;
font-family: var(--font-sans);
outline: none;
transition: border-color 0.2s;
}
.waitlist-form input:focus,
.waitlist-form select:focus {
border-color: var(--accent-primary);
}
.waitlist-form select {
cursor: pointer;
appearance: none;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 12px center;
background-repeat: no-repeat;
padding-right: 40px;
}
.waitlist-form button {
padding: 14px 28px;
border-radius: var(--radius-sm);
border: none;
background: var(--accent-gradient);
color: white;
font-size: 1rem;
font-weight: 600;
font-family: var(--font-sans);
cursor: pointer;
white-space: nowrap;
transition: opacity 0.2s, transform 0.1s;
}
.waitlist-form button:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
}
.waitlist-form button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.form-error {
color: var(--error);
font-size: 0.875rem;
margin-top: 8px;
}
/* Waitlist Success */
.waitlist-success {
text-align: center;
padding: 32px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
max-width: 400px;
}
.success-icon {
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(34, 197, 94, 0.1);
color: var(--success);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 700;
margin: 0 auto 16px;
}
.waitlist-success h3 {
margin-bottom: 8px;
font-size: 1.25rem;
}
.waitlist-success p {
color: var(--text-secondary);
font-size: 0.938rem;
}
/* Features Section */
.features {
padding: 120px 0;
background: var(--bg-secondary);
}
.features-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.feature-card {
padding: 32px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
transition: border-color 0.2s, transform 0.2s;
}
.feature-card:hover {
border-color: var(--border-light);
transform: translateY(-2px);
}
.feature-icon {
font-size: 2rem;
margin-bottom: 16px;
}
.feature-title {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 12px;
}
.feature-desc {
font-size: 0.938rem;
color: var(--text-secondary);
line-height: 1.7;
}
/* Tiers Section */
.tiers {
padding: 120px 0;
}
.tier-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-bottom: 64px;
}
.tier-card {
padding: 40px 32px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
position: relative;
display: flex;
flex-direction: column;
}
.tier-highlighted {
border-color: var(--accent-primary);
box-shadow: 0 0 30px rgba(59, 130, 246, 0.1);
transform: scale(1.05);
}
.tier-badge {
position: absolute;
top: -12px;
left: 50%;
transform: translateX(-50%);
padding: 4px 16px;
border-radius: 999px;
background: var(--accent-gradient);
color: white;
font-size: 0.813rem;
font-weight: 600;
white-space: nowrap;
}
.tier-name {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 12px;
}
.tier-price {
margin-bottom: 8px;
}
.price-value {
font-size: 3rem;
font-weight: 800;
}
.price-period {
font-size: 1rem;
color: var(--text-muted);
}
.tier-desc {
color: var(--text-secondary);
font-size: 0.938rem;
margin-bottom: 24px;
flex: 1;
}
.tier-card .waitlist-form {
max-width: 100%;
}
.tier-card .form-row {
flex-direction: column;
}
.tier-card .waitlist-form input {
width: 100%;
}
.tier-card .waitlist-form button {
width: 100%;
}
/* Tier Table */
.tier-table-wrapper {
overflow-x: auto;
}
.tier-table {
width: 100%;
border-collapse: collapse;
font-size: 0.938rem;
}
.tier-table th,
.tier-table td {
padding: 14px 20px;
text-align: center;
border-bottom: 1px solid var(--border-color);
}
.tier-table th {
font-weight: 600;
color: var(--text-secondary);
text-transform: uppercase;
font-size: 0.813rem;
letter-spacing: 0.05em;
}
.tier-table th:first-child,
.tier-table td:first-child {
text-align: left;
}
.tier-table td.feature-name {
color: var(--text-primary);
font-weight: 500;
}
.col-highlighted {
background: rgba(59, 130, 246, 0.05);
}
.check {
color: var(--success);
font-weight: 700;
}
.cross {
color: var(--text-muted);
}
/* Blog Preview */
.blog-preview {
padding: 120px 0;
background: var(--bg-secondary);
}
.blog-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-bottom: 48px;
}
.blog-card {
display: block;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
overflow: hidden;
transition: border-color 0.2s, transform 0.2s;
}
.blog-card:hover {
border-color: var(--border-light);
transform: translateY(-2px);
}
.blog-card-image {
height: 200px;
overflow: hidden;
}
.blog-card-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.blog-card-body {
padding: 24px;
}
.blog-card-tags {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.tag {
padding: 3px 10px;
border-radius: 999px;
background: rgba(59, 130, 246, 0.1);
color: var(--accent-primary);
font-size: 0.813rem;
font-weight: 500;
}
.blog-card-body h3 {
font-size: 1.125rem;
font-weight: 600;
margin-bottom: 8px;
line-height: 1.4;
}
.blog-card-body p {
font-size: 0.875rem;
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: 16px;
}
.blog-card-meta {
display: flex;
gap: 16px;
font-size: 0.813rem;
color: var(--text-muted);
}
.blog-placeholder {
text-align: center;
padding: 64px 24px;
background: var(--bg-card);
border: 1px dashed var(--border-color);
border-radius: var(--radius);
margin-bottom: 48px;
}
.blog-placeholder p {
color: var(--text-secondary);
font-size: 1rem;
}
.blog-cta {
text-align: center;
}
.btn-secondary {
display: inline-block;
padding: 12px 28px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-light);
color: var(--text-primary);
font-size: 0.938rem;
font-weight: 500;
font-family: var(--font-sans);
cursor: pointer;
transition: border-color 0.2s, background 0.2s;
}
.btn-secondary:hover {
border-color: var(--accent-primary);
background: rgba(59, 130, 246, 0.05);
}
/* Footer */
.footer {
padding: 80px 0 40px;
background: var(--bg-primary);
border-top: 1px solid var(--border-color);
}
.footer-grid {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1fr;
gap: 48px;
margin-bottom: 48px;
}
.footer-brand h3 {
font-size: 1.5rem;
font-weight: 800;
margin-bottom: 8px;
background: var(--accent-gradient);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.footer-brand p {
color: var(--text-muted);
font-size: 0.938rem;
max-width: 300px;
}
.footer-links h4 {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
color: var(--text-muted);
letter-spacing: 0.05em;
margin-bottom: 16px;
}
.footer-links a {
display: block;
color: var(--text-secondary);
font-size: 0.938rem;
padding: 4px 0;
transition: color 0.2s;
}
.footer-links a:hover {
color: var(--text-primary);
}
.footer-bottom {
padding-top: 32px;
border-top: 1px solid var(--border-color);
text-align: center;
}
.footer-bottom p {
color: var(--text-muted);
font-size: 0.875rem;
}
/* Blog Page */
.blog-page-header {
padding: 80px 0 48px;
background: var(--bg-secondary);
}
.back-link {
display: inline-block;
margin-bottom: 24px;
font-size: 0.938rem;
}
.blog-page-header h1 {
font-size: 3rem;
font-weight: 800;
margin-bottom: 16px;
}
.blog-page-header p {
color: var(--text-secondary);
font-size: 1.125rem;
max-width: 600px;
}
.blog-page-content {
padding: 64px 0;
}
.blog-list {
display: flex;
flex-direction: column;
gap: 24px;
}
.blog-list-item {
display: flex;
gap: 24px;
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius);
overflow: hidden;
transition: border-color 0.2s;
}
.blog-list-item:hover {
border-color: var(--border-light);
}
.blog-list-image {
width: 280px;
min-height: 180px;
flex-shrink: 0;
overflow: hidden;
}
.blog-list-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.blog-list-body {
padding: 24px;
flex: 1;
}
.blog-list-tags {
display: flex;
gap: 8px;
margin-bottom: 12px;
flex-wrap: wrap;
}
.blog-list-body h2 {
font-size: 1.5rem;
font-weight: 700;
margin-bottom: 12px;
}
.blog-list-body p {
color: var(--text-secondary);
font-size: 0.938rem;
line-height: 1.7;
margin-bottom: 16px;
}
.blog-list-meta {
display: flex;
gap: 16px;
font-size: 0.875rem;
color: var(--text-muted);
}
/* Blog Post */
.blog-post {
padding: 80px 0;
}
.blog-post-header {
margin-bottom: 48px;
}
.blog-post-header h1 {
font-size: 3rem;
font-weight: 800;
line-height: 1.2;
margin-bottom: 16px;
}
.blog-post-meta {
display: flex;
gap: 16px;
color: var(--text-muted);
font-size: 0.938rem;
margin-bottom: 16px;
}
.blog-post-tags {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.blog-post-cover {
border-radius: var(--radius);
overflow: hidden;
margin-bottom: 48px;
}
.blog-post-cover img {
width: 100%;
max-height: 500px;
object-fit: cover;
}
.blog-post-content {
max-width: 720px;
font-size: 1.063rem;
line-height: 1.8;
color: var(--text-secondary);
}
.blog-post-content h2 {
font-size: 1.75rem;
font-weight: 700;
color: var(--text-primary);
margin: 48px 0 16px;
}
.blog-post-content h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
margin: 32px 0 12px;
}
.blog-post-content p {
margin-bottom: 20px;
}
.blog-post-content ul,
.blog-post-content ol {
margin-bottom: 20px;
padding-left: 24px;
}
.blog-post-content li {
margin-bottom: 8px;
}
.blog-post-content code {
background: var(--bg-card);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.875em;
}
.blog-post-content pre {
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
padding: 20px;
overflow-x: auto;
margin-bottom: 24px;
}
.blog-post-content pre code {
background: none;
padding: 0;
}
.blog-post-content blockquote {
border-left: 3px solid var(--accent-primary);
padding-left: 20px;
color: var(--text-secondary);
font-style: italic;
margin: 24px 0;
}
/* Loading / Empty */
.loading {
text-align: center;
padding: 64px 0;
color: var(--text-muted);
}
.empty-state {
text-align: center;
padding: 64px 24px;
background: var(--bg-card);
border: 1px dashed var(--border-color);
border-radius: var(--radius);
}
.empty-state h2 {
margin-bottom: 12px;
}
.empty-state p {
color: var(--text-secondary);
}
/* Responsive */
@media (max-width: 1024px) {
.features-grid {
grid-template-columns: repeat(2, 1fr);
}
.tier-grid {
grid-template-columns: repeat(2, 1fr);
}
.tier-highlighted {
transform: none;
}
.blog-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.hero-title {
font-size: 2.5rem;
}
.hero-subtitle {
font-size: 1.063rem;
}
.hero-stats {
flex-direction: column;
gap: 24px;
margin-top: 48px;
}
.section-title {
font-size: 2rem;
}
.features-grid {
grid-template-columns: 1fr;
}
.tier-grid {
grid-template-columns: 1fr;
max-width: 400px;
margin-left: auto;
margin-right: auto;
}
.form-row {
flex-direction: column;
}
.footer-grid {
grid-template-columns: 1fr 1fr;
gap: 32px;
}
.blog-grid {
grid-template-columns: 1fr;
}
.blog-list-item {
flex-direction: column;
}
.blog-list-image {
width: 100%;
height: 200px;
}
.blog-page-header h1 {
font-size: 2rem;
}
.blog-post-header h1 {
font-size: 2rem;
}
}