FRE-709: Document duplicate recovery wake - FRE-635 already recovered via FRE-708

This commit is contained in:
2026-04-26 20:23:14 -04:00
parent e07237b6b0
commit 0ff6c74871
5880 changed files with 1643723 additions and 908 deletions

View File

@@ -353,3 +353,26 @@ export function useReferralCount(referralCode: string) {
enabled: !!referralCode,
}));
}
export function useBetaSignup() {
return createMutation(() => ({
mutationFn: async (input: {
name: string;
email: string;
primaryRole: string;
scriptsWritten?: string;
currentSoftware?: string;
softwareLove?: string;
softwareFrustrate?: string;
hoursPerWeek?: string;
willingFeedback?: string;
joinDiscord?: string;
discordUsername?: string;
excitedFeatures?: string[];
heardAbout?: string;
additionalInfo?: string;
}) => {
return await trpc.beta.signup.mutate(input);
},
}));
}

View File

@@ -4,6 +4,7 @@ import { ProtectedRoute } from './components/auth/ProtectedRoute';
import { SignIn } from './components/auth/SignIn';
import { SignUp } from './components/auth/SignUp';
import { Landing } from './routes/landing/Landing';
import { BetaSignup } from './routes/beta/BetaSignup';
import { Blog } from './routes/blog/Blog';
import { BlogPost } from './routes/blog/BlogPost';
import { Features } from './routes/features/Features';
@@ -30,6 +31,7 @@ const Redirect = () => <Navigate href="/dashboard" />;
export const routes = [
<Route path="/" component={Landing} />,
<Route path="/beta" component={BetaSignup} />,
<Route path="/features" component={Features} />,
<Route path="/pricing" component={Pricing} />,
<Route path="/about" component={About} />,

View File

@@ -0,0 +1,403 @@
import { Component, createSignal } from 'solid-js';
import { A } from '@solidjs/router';
import { useBetaSignup } from '../../lib/api/trpc-hooks';
import '../../styles/beta-signup.css';
export const BetaSignup: Component = () => {
const [formData, setFormData] = createSignal({
name: '',
email: '',
primaryRole: '',
scriptsWritten: '',
currentSoftware: '',
softwareLove: '',
softwareFrustrate: '',
hoursPerWeek: '',
willingFeedback: '',
joinDiscord: '',
discordUsername: '',
excitedFeatures: [] as string[],
heardAbout: '',
additionalInfo: '',
});
const [submitted, setSubmitted] = createSignal(false);
const [error, setError] = createSignal('');
const [isSubmitting, setIsSubmitting] = createSignal(false);
const betaSignup = useBetaSignup();
const updateField = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value }));
};
const toggleFeature = (feature: string) => {
setFormData((prev) => {
const features = prev.excitedFeatures.includes(feature)
? prev.excitedFeatures.filter((f) => f !== feature)
: [...prev.excitedFeatures, feature];
return { ...prev, excitedFeatures: features };
});
};
const handleSubmit = async (e: Event) => {
e.preventDefault();
setError('');
const data = formData();
if (!data.name.trim() || !data.email.trim()) {
setError('Name and email are required.');
return;
}
if (!data.primaryRole) {
setError('Please select your primary role.');
return;
}
if (!data.willingFeedback || data.willingFeedback === 'No, just want early access') {
setError('Beta access requires willingness to provide weekly feedback.');
return;
}
setIsSubmitting(true);
try {
await betaSignup.mutateAsync({
name: data.name.trim(),
email: data.email.trim(),
primaryRole: data.primaryRole,
scriptsWritten: data.scriptsWritten,
currentSoftware: data.currentSoftware,
softwareLove: data.softwareLove,
softwareFrustrate: data.softwareFrustrate,
hoursPerWeek: data.hoursPerWeek,
willingFeedback: data.willingFeedback,
joinDiscord: data.joinDiscord,
discordUsername: data.discordUsername,
excitedFeatures: data.excitedFeatures,
heardAbout: data.heardAbout,
additionalInfo: data.additionalInfo,
});
setSubmitted(true);
} catch (err: any) {
setError(err?.message || 'Something went wrong. Please try again.');
} finally {
setIsSubmitting(false);
}
};
return (
<div class="beta-signup-page">
<nav class="landing-nav">
<div class="nav-container">
<div class="nav-logo">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none">
<path d="M16 2L4 8V24L16 30L28 24V8L16 2Z" fill="#518ac8"/>
<path d="M16 6L8 10V22L16 26L24 22V10L16 6Z" fill="#76b3e1"/>
</svg>
<span class="logo-text">Scripter</span>
</div>
<div class="nav-links">
<a href="/">Home</a>
<a href="/features">Features</a>
<a href="/pricing">Pricing</a>
</div>
</div>
</nav>
<div class="beta-hero">
<h1>Join the Scripter Beta</h1>
<p>Help us build the future of screenwriting. We're looking for 500 active writers to test Scripter before our public launch.</p>
<div class="beta-badges">
<span class="beta-badge">🎬 3-week beta program</span>
<span class="beta-badge">📝 Weekly feedback (5 min)</span>
<span class="beta-badge">💬 Discord community</span>
</div>
</div>
{submitted() ? (
<div class="beta-success">
<div class="success-icon">🎉</div>
<h2>Application Submitted!</h2>
<p>Thanks for applying to the Scripter beta. We're reviewing applications and will get back to you within 48 hours.</p>
<div class="success-next-steps">
<h3>What happens next:</h3>
<ol>
<li>We'll review your application</li>
<li>If accepted, you'll get beta access + Discord invite</li>
<li>Beta starts April 26 - get ready to write!</li>
</ol>
</div>
<div class="success-actions">
<a href="https://twitter.com/ScripterApp" target="_blank" rel="noopener noreferrer" class="btn-secondary">
Follow us on Twitter
</a>
<A href="/" class="btn-primary">Back to Home</A>
</div>
</div>
) : (
<form onSubmit={handleSubmit} class="beta-form">
{error() && <div class="form-error-banner">{error()}</div>}
<section class="form-section">
<h2>Section 1: About You</h2>
<div class="form-group">
<label for="name">What's your name? *</label>
<input
type="text"
id="name"
value={formData().name}
onInput={(e) => updateField('name', e.currentTarget.value)}
required
/>
</div>
<div class="form-group">
<label for="email">What's your email address? *</label>
<input
type="email"
id="email"
value={formData().email}
onInput={(e) => updateField('email', e.currentTarget.value)}
required
/>
</div>
<div class="form-group">
<label for="primaryRole">What's your primary role? *</label>
<select
id="primaryRole"
value={formData().primaryRole}
onChange={(e) => updateField('primaryRole', e.currentTarget.value)}
required
>
<option value="">Select your role</option>
<option value="Screenwriter (feature films)">Screenwriter (feature films)</option>
<option value="Screenwriter (TV/Streaming)">Screenwriter (TV/Streaming)</option>
<option value="Writer/Director">Writer/Director</option>
<option value="Producer">Producer</option>
<option value="Student">Student</option>
<option value="Other">Other</option>
</select>
</div>
<div class="form-group">
<label for="scriptsWritten">How many scripts have you written?</label>
<select
id="scriptsWritten"
value={formData().scriptsWritten}
onChange={(e) => updateField('scriptsWritten', e.currentTarget.value)}
>
<option value="">Select an option</option>
<option value="0-1 (just starting)">0-1 (just starting)</option>
<option value="2-5 (developing craft)">2-5 (developing craft)</option>
<option value="6-10 (working writer)">6-10 (working writer)</option>
<option value="10+ (professional)">10+ (professional)</option>
</select>
</div>
</section>
<section class="form-section">
<h2>Section 2: Current Tools</h2>
<div class="form-group">
<label for="currentSoftware">What screenwriting software do you currently use?</label>
<select
id="currentSoftware"
value={formData().currentSoftware}
onChange={(e) => updateField('currentSoftware', e.currentTarget.value)}
>
<option value="">Select software</option>
<option value="Final Draft">Final Draft</option>
<option value="WriterDuet">WriterDuet</option>
<option value="Celtx">Celtx</option>
<option value="Fade In">Fade In</option>
<option value="Arc Studio">Arc Studio</option>
<option value="Google Docs">Google Docs</option>
<option value="Microsoft Word">Microsoft Word</option>
<option value="Other">Other</option>
</select>
</div>
<div class="form-group">
<label for="softwareLove">What do you love about your current tool? *</label>
<textarea
id="softwareLove"
value={formData().softwareLove}
onInput={(e) => updateField('softwareLove', e.currentTarget.value)}
rows={3}
required
/>
</div>
<div class="form-group">
<label for="softwareFrustrate">What frustrates you about your current tool? *</label>
<textarea
id="softwareFrustrate"
value={formData().softwareFrustrate}
onInput={(e) => updateField('softwareFrustrate', e.currentTarget.value)}
rows={3}
required
/>
</div>
</section>
<section class="form-section">
<h2>Section 3: Beta Commitment</h2>
<div class="form-group">
<label for="hoursPerWeek">How many hours per week do you spend screenwriting?</label>
<select
id="hoursPerWeek"
value={formData().hoursPerWeek}
onChange={(e) => updateField('hoursPerWeek', e.currentTarget.value)}
>
<option value="">Select an option</option>
<option value="0-5 (hobbyist)">0-5 (hobbyist)</option>
<option value="5-10 (serious amateur)">5-10 (serious amateur)</option>
<option value="10-20 (working writer)">10-20 (working writer)</option>
<option value="20+ (professional)">20+ (professional)</option>
</select>
</div>
<div class="form-group">
<label for="willingFeedback">Are you willing to provide weekly feedback (5-min survey)? *</label>
<select
id="willingFeedback"
value={formData().willingFeedback}
onChange={(e) => updateField('willingFeedback', e.currentTarget.value)}
required
>
<option value="">Select an option</option>
<option value="Yes, absolutely">Yes, absolutely (required to join beta)</option>
<option value="No, just want early access">No, just want early access</option>
<option value="Maybe, depends on my schedule">Maybe, depends on my schedule</option>
</select>
</div>
<div class="form-group">
<label for="joinDiscord">Will you join our Discord community?</label>
<select
id="joinDiscord"
value={formData().joinDiscord}
onChange={(e) => updateField('joinDiscord', e.currentTarget.value)}
>
<option value="">Select an option</option>
<option value="Yes, I'll join">Yes, I'll join</option>
<option value="No, email is fine">No, email is fine</option>
<option value="Maybe">Maybe</option>
</select>
</div>
<div class="form-group">
<label for="discordUsername">Discord username (if joining)</label>
<input
type="text"
id="discordUsername"
value={formData().discordUsername}
onInput={(e) => updateField('discordUsername', e.currentTarget.value)}
placeholder="username#1234"
/>
</div>
</section>
<section class="form-section">
<h2>Section 4: Use Cases</h2>
<div class="form-group">
<label>What features are you most excited about?</label>
<div class="checkbox-group">
{['Real-time collaboration', 'AI writing assistant', 'Cloud sync across devices', 'Affordable pricing', 'Modern interface', 'Export options (PDF, FDX, etc.)'].map((feature) => (
<label class="checkbox-label">
<input
type="checkbox"
checked={formData().excitedFeatures.includes(feature)}
onChange={() => toggleFeature(feature)}
/>
{feature}
</label>
))}
</div>
</div>
<div class="form-group">
<label for="heardAbout">How did you hear about Scripter?</label>
<select
id="heardAbout"
value={formData().heardAbout}
onChange={(e) => updateField('heardAbout', e.currentTarget.value)}
>
<option value="">Select an option</option>
<option value="Product Hunt">Product Hunt</option>
<option value="Reddit">Reddit</option>
<option value="Twitter/X">Twitter/X</option>
<option value="YouTube">YouTube</option>
<option value="Friend/colleague">Friend/colleague</option>
<option value="Google search">Google search</option>
<option value="Other">Other</option>
</select>
</div>
<div class="form-group">
<label for="additionalInfo">Anything else you'd like us to know?</label>
<textarea
id="additionalInfo"
value={formData().additionalInfo}
onInput={(e) => updateField('additionalInfo', e.currentTarget.value)}
rows={3}
placeholder="Optional"
/>
</div>
</section>
<div class="form-submit">
<button type="submit" class="btn-primary btn-large" disabled={isSubmitting()}>
{isSubmitting() ? 'Submitting...' : 'Submit Application'}
</button>
<p class="form-note">
By submitting, you agree to provide weekly feedback during the 3-week beta period.
</p>
</div>
</form>
)}
<footer class="landing-footer">
<div class="footer-content">
<div class="footer-brand">
<div class="nav-logo">
<svg width="24" height="24" viewBox="0 0 32 32" fill="none">
<path d="M16 2L4 8V24L16 30L28 24V8L16 2Z" fill="#518ac8"/>
</svg>
<span>Scripter</span>
</div>
<p>Write Faster.</p>
</div>
<div class="footer-links">
<div class="footer-col">
<h4>Product</h4>
<a href="/features">Features</a>
<a href="/pricing">Pricing</a>
<a href="/blog">Blog</a>
</div>
<div class="footer-col">
<h4>Company</h4>
<a href="/about">About</a>
<a href="/faq">FAQ</a>
</div>
<div class="footer-col">
<h4>Legal</h4>
<a href="/terms">Terms</a>
<a href="/privacy">Privacy</a>
</div>
</div>
</div>
<div class="footer-bottom">
<p>&copy; 2026 Scripter. All rights reserved.</p>
</div>
</footer>
</div>
);
};

303
src/styles/beta-signup.css Normal file
View File

@@ -0,0 +1,303 @@
/* Beta Signup Page Styles */
.beta-signup-page {
min-height: 100vh;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8ec 100%);
}
.beta-hero {
max-width: 800px;
margin: 0 auto;
padding: 60px 20px 40px;
text-align: center;
}
.beta-hero h1 {
font-size: 3rem;
font-weight: 800;
color: #1a1a1a;
margin-bottom: 16px;
letter-spacing: -0.02em;
}
.beta-hero p {
font-size: 1.25rem;
color: #666;
line-height: 1.6;
margin-bottom: 24px;
}
.beta-badges {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
margin-top: 24px;
}
.beta-badge {
background: white;
padding: 8px 16px;
border-radius: 24px;
font-size: 0.9rem;
font-weight: 500;
color: #333;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.beta-form {
max-width: 700px;
margin: 0 auto 60px;
background: white;
border-radius: 16px;
padding: 40px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.form-section {
margin-bottom: 40px;
padding-bottom: 30px;
border-bottom: 1px solid #e0e0e0;
}
.form-section:last-of-type {
border-bottom: none;
margin-bottom: 20px;
}
.form-section h2 {
font-size: 1.5rem;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 24px;
padding-bottom: 12px;
border-bottom: 2px solid #518ac8;
display: inline-block;
}
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
font-weight: 600;
color: #333;
margin-bottom: 8px;
font-size: 0.95rem;
}
.form-group input[type="text"],
.form-group input[type="email"],
.form-group select,
.form-group textarea {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 1rem;
font-family: inherit;
transition: border-color 0.2s, box-shadow 0.2s;
background: white;
}
.form-group input:focus,
.form-group select:focus,
.form-group textarea:focus {
outline: none;
border-color: #518ac8;
box-shadow: 0 0 0 3px rgba(81, 138, 200, 0.1);
}
.form-group textarea {
resize: vertical;
min-height: 100px;
}
.checkbox-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 12px;
margin-top: 12px;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
padding: 10px 14px;
background: #f8f9fa;
border-radius: 8px;
transition: background 0.2s;
font-weight: normal;
}
.checkbox-label:hover {
background: #e8eaed;
}
.checkbox-label input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
accent-color: #518ac8;
}
.form-submit {
text-align: center;
margin-top: 40px;
}
.btn-large {
padding: 16px 48px;
font-size: 1.1rem;
font-weight: 600;
}
.form-note {
margin-top: 16px;
font-size: 0.9rem;
color: #666;
}
.form-error-banner {
background: #fee;
border: 1px solid #fcc;
color: #c00;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 24px;
font-weight: 500;
}
.beta-success {
max-width: 600px;
margin: 60px auto;
background: white;
border-radius: 16px;
padding: 60px 40px;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
}
.beta-success .success-icon {
font-size: 4rem;
margin-bottom: 20px;
}
.beta-success h2 {
font-size: 2rem;
font-weight: 700;
color: #1a1a1a;
margin-bottom: 16px;
}
.beta-success > p {
font-size: 1.1rem;
color: #666;
margin-bottom: 32px;
line-height: 1.6;
}
.success-next-steps {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
text-align: left;
}
.success-next-steps h3 {
font-size: 1.2rem;
font-weight: 600;
color: #333;
margin-bottom: 16px;
}
.success-next-steps ol {
margin: 0;
padding-left: 24px;
color: #555;
line-height: 2;
}
.success-next-steps li {
margin-bottom: 8px;
}
.success-actions {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
}
.btn-primary {
display: inline-block;
background: #518ac8;
color: white;
padding: 12px 32px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
transition: background 0.2s, transform 0.1s;
border: none;
cursor: pointer;
}
.btn-primary:hover {
background: #4a7ab8;
transform: translateY(-1px);
}
.btn-primary:disabled {
background: #999;
cursor: not-allowed;
transform: none;
}
.btn-secondary {
display: inline-block;
background: white;
color: #518ac8;
padding: 12px 32px;
border-radius: 8px;
text-decoration: none;
font-weight: 600;
border: 2px solid #518ac8;
transition: background 0.2s, transform 0.1s;
}
.btn-secondary:hover {
background: #f0f5fa;
transform: translateY(-1px);
}
/* Responsive */
@media (max-width: 768px) {
.beta-hero h1 {
font-size: 2rem;
}
.beta-hero p {
font-size: 1rem;
}
.beta-form {
padding: 24px;
}
.beta-badges {
flex-direction: column;
align-items: center;
}
.checkbox-group {
grid-template-columns: 1fr;
}
.success-actions {
flex-direction: column;
}
}