Implement waitlist landing page FRE-656

- Add waitlist tRPC router with signup mutation and count query
- Add referral code generation and tracking
- Register waitlist router in app router
- Add useWaitlistSignup, useWaitlistCount, useReferralCount hooks
- Update landing page with email capture form, live waitlist counter, referral sharing
- Add waitlist and referral CSS styles

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-04-26 07:57:29 -04:00
parent 11a188c68e
commit ec215ae426
5 changed files with 380 additions and 11 deletions

View File

@@ -324,3 +324,32 @@ export function useDeleteScene() {
},
}));
}
// Waitlist hooks
export function useWaitlistSignup() {
return createMutation(() => ({
mutationFn: async (input: { email: string; name?: string; source?: string; referralCode?: string }) => {
return await trpc.waitlist.signup.mutate(input);
},
}));
}
export function useWaitlistCount() {
return createQuery(() => ({
queryKey: ['waitlistCount'],
queryFn: async () => {
return await trpc.waitlist.getCount.query(undefined);
},
refetchInterval: 30000,
}));
}
export function useReferralCount(referralCode: string) {
return createQuery(() => ({
queryKey: ['referralCount', referralCode],
queryFn: async () => {
return await trpc.waitlist.getReferralCount.query({ referralCode });
},
enabled: !!referralCode,
}));
}

View File

@@ -1,7 +1,76 @@
import { Component, createSignal } from 'solid-js';
import { A } from '@solidjs/router';
import { Component, createSignal, onMount } from 'solid-js';
import { A, useSearchParams } from '@solidjs/router';
import { useWaitlistSignup, useWaitlistCount } from '../../lib/api/trpc-hooks';
export const Landing: Component = () => {
const [searchParams] = useSearchParams();
const [email, setEmail] = createSignal('');
const [name, setName] = createSignal('');
const [submitted, setSubmitted] = createSignal(false);
const [error, setError] = createSignal('');
const [referralCode, setReferralCode] = createSignal('');
const [myReferralCode, setMyReferralCode] = createSignal('');
const signup = useWaitlistSignup();
const count = useWaitlistCount();
onMount(() => {
const ref = searchParams.ref;
if (ref && typeof ref === 'string') {
setReferralCode(ref);
}
});
const handleSubmit = async (e: Event) => {
e.preventDefault();
setError('');
if (!email().trim()) {
setError('Please enter your email address.');
return;
}
try {
const result = await signup.mutateAsync({
email: email().trim(),
name: name().trim() || undefined,
source: referralCode() ? 'referral' : 'organic',
referralCode: referralCode() || undefined,
});
if (result.referralCode) {
setMyReferralCode(result.referralCode);
}
setSubmitted(true);
} catch (err: any) {
setError(err?.message || 'Something went wrong. Please try again.');
}
};
const waitlistCount = () => {
const c = count.data?.count;
if (!c) return '0';
if (c >= 1000) return `${(c / 1000).toFixed(1)}k+`;
return String(c);
};
const shareUrl = () => {
if (!myReferralCode()) return '';
return `${window.location.origin}?ref=${myReferralCode()}`;
};
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(shareUrl());
} catch {
const input = document.createElement('input');
input.value = shareUrl();
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
}
};
return (
<div class="landing-page">
{/* Navigation */}
@@ -19,7 +88,6 @@ export const Landing: Component = () => {
<a href="#pricing">Pricing</a>
<a href="/blog">Blog</a>
<A href="/sign-in" class="nav-signin">Sign In</A>
<A href="/sign-up" class="nav-signup">Start Writing Free</A>
</div>
</div>
</nav>
@@ -29,13 +97,57 @@ export const Landing: Component = () => {
<div class="hero-content">
<h1 class="hero-headline">Write Faster.</h1>
<p class="hero-subheadline">
The modern screenwriting platform built for how you actually work.
The modern screenwriting platform built for how you actually work.
Real-time collaboration, AI-powered writing, and industry-standard formatting all in one place.
</p>
<div class="hero-cta">
<A href="/sign-up" class="cta-primary">Start Writing Free</A>
<p class="cta-note">No credit card required</p>
</div>
{submitted() ? (
<div class="waitlist-success">
<div class="success-icon">🎉</div>
<h3>You're on the list!</h3>
<p>We'll notify you when Scripter launches. In the meantime, spread the word.</p>
{myReferralCode() && (
<div class="referral-share">
<p class="referral-label">Share your unique referral link:</p>
<div class="referral-link-box">
<input type="text" value={shareUrl()} readonly class="referral-input" />
<button onClick={copyToClipboard} class="copy-btn">Copy</button>
</div>
<p class="referral-hint">Earn early access perks for every friend who joins!</p>
</div>
)}
<A href="/sign-up" class="cta-primary">Explore the App</A>
</div>
) : (
<form onSubmit={handleSubmit} class="waitlist-form">
<div class="form-row">
<input
type="text"
placeholder="Your name (optional)"
value={name()}
onInput={(e) => setName(e.currentTarget.value)}
class="form-input"
/>
</div>
<div class="form-row">
<input
type="email"
placeholder="Enter your email"
value={email()}
onInput={(e) => setEmail(e.currentTarget.value)}
class="form-input"
required
/>
<button type="submit" class="cta-primary" disabled={signup.isPending}>
{signup.isPending ? 'Joining...' : 'Join the Waitlist'}
</button>
</div>
{error() && <p class="form-error">{error()}</p>}
{referralCode() && (
<p class="referral-notice">You were referred by a friend!</p>
)}
</form>
)}
</div>
<div class="hero-visual">
<div class="screenshot-mockup">
@@ -63,11 +175,13 @@ She doesn't look up. In the zone.`}</pre>
{/* Social Proof */}
<section class="social-proof">
<p>Trusted by screenwriters everywhere</p>
<p>Join the growing community of screenwriters</p>
<div class="proof-badges">
<span class="badge counter-badge">
<span class="counter-number">{waitlistCount()}</span> writers on the waitlist
</span>
<span class="badge">🎬 Industry Standard</span>
<span class="badge"> 5-Star Reviews</span>
<span class="badge">🚀 Fastest Growing</span>
</div>
</section>
@@ -122,7 +236,7 @@ She doesn't look up. In the zone.`}</pre>
<section id="pricing" class="pricing">
<h2 class="section-title">Simple pricing for every screenwriter</h2>
<p class="section-subtitle">Start free. Upgrade when you need more.</p>
<div class="pricing-grid">
<div class="pricing-card">
<h3>Free</h3>

View File

@@ -531,7 +531,143 @@
font-size: 0.875rem;
}
/* Waitlist Form */
.waitlist-form {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-width: 480px;
}
.form-row {
display: flex;
gap: 0.5rem;
}
.form-input {
flex: 1;
padding: 0.875rem 1rem;
border: 2px solid #d1d5db;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s;
outline: none;
background: white;
color: #1a1a1a;
}
.form-input:focus {
border-color: #518ac8;
}
.form-error {
color: #ef4444;
font-size: 0.875rem;
margin: 0;
}
.referral-notice {
font-size: 0.875rem;
color: #518ac8;
margin: 0;
font-weight: 500;
}
/* Waitlist Success */
.waitlist-success {
text-align: center;
padding: 1rem;
}
.waitlist-success .success-icon {
font-size: 3rem;
margin-bottom: 0.5rem;
}
.waitlist-success h3 {
font-size: 1.5rem;
color: #1a336b;
margin: 0 0 0.5rem;
}
.waitlist-success p {
color: #666;
margin: 0 0 1.5rem;
line-height: 1.5;
}
/* Referral Share */
.referral-share {
background: #f0f7ff;
border: 1px solid #b3d4f0;
border-radius: 12px;
padding: 1.25rem;
margin-bottom: 1.5rem;
}
.referral-label {
font-weight: 600;
color: #1a336b;
margin-bottom: 0.75rem !important;
}
.referral-link-box {
display: flex;
gap: 0.5rem;
}
.referral-link-box .referral-input {
flex: 1;
padding: 0.625rem 0.75rem;
border: 1px solid #b3d4f0;
border-radius: 6px;
font-size: 0.875rem;
background: white;
color: #1a1a1a;
}
.copy-btn {
padding: 0.625rem 1rem;
background: #518ac8;
color: white;
border: none;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
.copy-btn:hover {
background: #3a6ca8;
}
.referral-hint {
font-size: 0.8rem;
color: #6b7280;
margin-top: 0.75rem !important;
margin-bottom: 0 !important;
}
/* Counter Badge */
.counter-badge {
background: linear-gradient(135deg, #1a336b, #518ac8);
color: white !important;
}
.counter-number {
font-size: 1.25rem;
font-weight: 700;
}
/* Hero CTA button when used as form submit */
.hero-cta .cta-primary:disabled {
opacity: 0.7;
cursor: not-allowed;
}
/* Responsive */
@media (max-width: 968px) {
.hero {
grid-template-columns: 1fr;