feat: add daily product intelligence and versioned site assets (#64)
Adds privacy-safe aggregate product intelligence, reviewed/versioned website assets, and a dispatch-only rollout until the dedicated Sentry token is verified. Independent review blockers were fixed in 8bc47e4; app checks, website production build, Android CI, and iOS CI are green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
18
website/app/beta/layout.tsx
Normal file
18
website/app/beta/layout.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Join the Beta',
|
||||
description:
|
||||
'Sign up as a beta tester for OpenCode Mobile on Google Play. Help us test, find bugs, and shape the product before public launch.',
|
||||
alternates: { canonical: 'https://opencode.agentlabs.cc/beta' },
|
||||
openGraph: {
|
||||
title: 'Join the OpenCode Mobile Beta',
|
||||
description:
|
||||
'Get early access to OpenCode Mobile on Google Play. We need 20 testers to unlock the public release.',
|
||||
url: 'https://opencode.agentlabs.cc/beta',
|
||||
},
|
||||
}
|
||||
|
||||
export default function BetaLayout({ children }: { children: React.ReactNode }) {
|
||||
return children
|
||||
}
|
||||
184
website/app/beta/page.tsx
Normal file
184
website/app/beta/page.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
'use client'
|
||||
|
||||
import { useState, FormEvent } from 'react'
|
||||
|
||||
export default function BetaPage() {
|
||||
const [email, setEmail] = useState('')
|
||||
const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle')
|
||||
const [message, setMessage] = useState('')
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault()
|
||||
setStatus('loading')
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/beta-signup', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email }),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
setStatus('success')
|
||||
setMessage('You\'re in! We\'ll add you to the closed beta testers list.')
|
||||
setEmail('')
|
||||
} else {
|
||||
const data = await res.json()
|
||||
setStatus('error')
|
||||
setMessage(data.error || 'Something went wrong. Please try again.')
|
||||
}
|
||||
} catch {
|
||||
setStatus('error')
|
||||
setMessage('Network error. Please try again.')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
<section className="pt-24 pb-16 md:pt-36 md:pb-24">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6">
|
||||
<p className="text-xs font-mono uppercase tracking-wider mb-6" style={{ color: 'var(--muted)' }}>
|
||||
Closed beta · Limited spots
|
||||
</p>
|
||||
|
||||
<h1 className="text-4xl sm:text-5xl font-bold leading-[1.1] tracking-tight mb-6" style={{ color: 'var(--fg)' }}>
|
||||
Get early access
|
||||
</h1>
|
||||
|
||||
<p className="text-lg leading-relaxed mb-10" style={{ color: 'var(--muted)' }}>
|
||||
Sign up to join the OpenCode Mobile closed beta on Google Play.
|
||||
We need <span style={{ color: 'var(--fg)' }}>20 testers</span> to unlock the public release.
|
||||
</p>
|
||||
|
||||
{/* Signup Form */}
|
||||
{status === 'success' ? (
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: 'var(--green)' }}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<p className="font-semibold" style={{ color: 'var(--fg)' }}>You're on the list</p>
|
||||
</div>
|
||||
<p className="text-sm" style={{ color: 'var(--muted)' }}>
|
||||
{message}
|
||||
</p>
|
||||
<p className="mt-3 text-sm" style={{ color: 'var(--muted)' }}>
|
||||
We'll send you a Google Play beta invite within 24 hours.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="flex flex-col sm:flex-row gap-3">
|
||||
<input
|
||||
type="email"
|
||||
required
|
||||
placeholder="your@gmail.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="flex-1 px-4 py-3 rounded-lg text-sm outline-none transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--card)',
|
||||
border: '1px solid var(--border)',
|
||||
color: 'var(--fg)',
|
||||
}}
|
||||
disabled={status === 'loading'}
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-primary whitespace-nowrap text-sm"
|
||||
disabled={status === 'loading'}
|
||||
>
|
||||
{status === 'loading' ? 'Signing up...' : 'Join beta'}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{status === 'error' && (
|
||||
<p className="mt-3 text-sm" style={{ color: '#EF4444' }}>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<p className="mt-4 text-xs" style={{ color: 'var(--muted)' }}>
|
||||
Use the Gmail linked to your Google Play account. No spam, no commitment.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* What you get */}
|
||||
<section className="py-20 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6">
|
||||
<h2 className="text-2xl font-bold mb-8" style={{ color: 'var(--fg)' }}>
|
||||
What beta testers get
|
||||
</h2>
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
title: 'Early access',
|
||||
desc: 'Install from Google Play before anyone else. Get updates as we ship them.',
|
||||
},
|
||||
{
|
||||
title: 'Direct feedback channel',
|
||||
desc: 'Report bugs and request features directly to the dev team.',
|
||||
},
|
||||
{
|
||||
title: 'Shape the product',
|
||||
desc: 'Your input decides what we build next. Beta feedback has outsized impact.',
|
||||
},
|
||||
].map((item) => (
|
||||
<div key={item.title} className="flex gap-4">
|
||||
<span className="text-sm font-mono shrink-0 mt-0.5" style={{ color: 'var(--muted)' }}>[*]</span>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1" style={{ color: 'var(--fg)' }}>{item.title}</h3>
|
||||
<p className="text-sm leading-relaxed" style={{ color: 'var(--muted)' }}>{item.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* FAQ */}
|
||||
<section className="py-20 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6">
|
||||
<h2 className="text-2xl font-bold mb-8" style={{ color: 'var(--fg)' }}>
|
||||
FAQ
|
||||
</h2>
|
||||
<div className="space-y-0">
|
||||
{[
|
||||
{
|
||||
q: 'What is OpenCode Mobile?',
|
||||
a: 'A free, open-source mobile client for the opencode AI coding agent. Connect to your own server and code from anywhere.',
|
||||
},
|
||||
{
|
||||
q: 'Why do you need my email?',
|
||||
a: 'Google Play requires us to add tester email addresses explicitly. We use the same Gmail you use on your Android device.',
|
||||
},
|
||||
{
|
||||
q: 'Is it free?',
|
||||
a: 'Yes. Free and open source, MIT license. No in-app purchases, no ads.',
|
||||
},
|
||||
{
|
||||
q: 'When does the public release happen?',
|
||||
a: 'Google requires 14 days of closed testing with at least 20 testers. The sooner we hit 20, the sooner we launch.',
|
||||
},
|
||||
{
|
||||
q: 'What Android version?',
|
||||
a: 'Android 8.0 (API 26) or newer.',
|
||||
},
|
||||
].map((faq) => (
|
||||
<div key={faq.q} className="py-5 border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<h3 className="text-sm font-semibold mb-2" style={{ color: 'var(--fg)' }}>
|
||||
{faq.q}
|
||||
</h3>
|
||||
<p className="text-sm" style={{ color: 'var(--muted)' }}>
|
||||
{faq.a}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user