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>
3
website/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.next
|
||||
.vercel
|
||||
44
website/app/api/beta-signup/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
|
||||
export const runtime = 'edge'
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email } = await request.json()
|
||||
|
||||
if (!email || typeof email !== 'string') {
|
||||
return NextResponse.json({ error: 'Email is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const normalized = email.trim().toLowerCase()
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalized)) {
|
||||
return NextResponse.json({ error: 'Invalid email address' }, { status: 400 })
|
||||
}
|
||||
|
||||
// Log for Vercel Function Logs (always available)
|
||||
console.log(JSON.stringify({
|
||||
event: 'beta_signup',
|
||||
email: normalized,
|
||||
timestamp: new Date().toISOString(),
|
||||
ip: request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip'),
|
||||
}))
|
||||
|
||||
// If a webhook URL is configured, forward the signup there
|
||||
const webhookUrl = process.env.BETA_SIGNUP_WEBHOOK_URL
|
||||
if (webhookUrl) {
|
||||
try {
|
||||
await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: normalized, source: 'opencode-mobile-beta' }),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[beta-signup] Webhook failed:', e)
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ message: 'Signed up successfully' })
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request' }, { status: 400 })
|
||||
}
|
||||
}
|
||||
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
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
106
website/app/docs/page.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Documentation',
|
||||
description:
|
||||
'OpenCode Mobile documentation. Quick start guide, API reference, and self-hosting instructions.',
|
||||
alternates: { canonical: 'https://opencode.agentlabs.cc/docs' },
|
||||
openGraph: {
|
||||
title: 'Documentation | OpenCode Mobile',
|
||||
description: 'OpenCode Mobile docs — coming soon. See README on GitHub for now.',
|
||||
url: 'https://opencode.agentlabs.cc/docs',
|
||||
},
|
||||
}
|
||||
|
||||
const quickLinks = [
|
||||
{
|
||||
title: 'README',
|
||||
description: 'Installation, building from source, and quick start guide.',
|
||||
href: 'https://github.com/dzianisv/opencode-mobile#readme',
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
title: 'Releases',
|
||||
description: 'Download APKs and view changelog for every release.',
|
||||
href: 'https://github.com/dzianisv/opencode-mobile/releases',
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
title: 'Bug Reports',
|
||||
description: 'Found a bug? Open an issue on GitHub.',
|
||||
href: 'https://github.com/dzianisv/opencode-mobile/issues/new',
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
title: 'Contributing',
|
||||
description: 'Want to contribute? See CONTRIBUTING.md in the repo.',
|
||||
href: 'https://github.com/dzianisv/opencode-mobile/blob/main/CONTRIBUTING.md',
|
||||
external: true,
|
||||
},
|
||||
]
|
||||
|
||||
export default function DocsPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-16">
|
||||
<div className="text-center mb-16">
|
||||
<div className="w-20 h-20 rounded-2xl flex items-center justify-center mx-auto mb-6"
|
||||
style={{ backgroundColor: 'rgba(59,130,246,0.15)', color: 'var(--accent)' }}>
|
||||
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="text-3xl font-bold mb-4" style={{ color: 'var(--fg)' }}>
|
||||
Documentation
|
||||
</h1>
|
||||
<p className="text-lg max-w-xl mx-auto" style={{ color: 'var(--muted)' }}>
|
||||
A full documentation site is in progress. For now, everything you need is in the
|
||||
GitHub repository.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5 mb-16">
|
||||
{quickLinks.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target={link.external ? '_blank' : undefined}
|
||||
rel={link.external ? 'noopener noreferrer' : undefined}
|
||||
className="card hover:border-blue-500/60 transition-colors"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<h3 className="font-semibold mb-2 flex items-center gap-2" style={{ color: 'var(--fg)' }}>
|
||||
{link.title}
|
||||
{link.external && (
|
||||
<svg className="w-3.5 h-3.5 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
)}
|
||||
</h3>
|
||||
<p className="text-sm" style={{ color: 'var(--muted)' }}>{link.description}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl p-6 text-center" style={{ background: 'rgba(59,130,246,0.08)', border: '1px solid rgba(59,130,246,0.25)' }}>
|
||||
<h2 className="font-semibold mb-2" style={{ color: 'var(--fg)' }}>
|
||||
Full docs site coming soon
|
||||
</h2>
|
||||
<p className="text-sm mb-4" style={{ color: 'var(--muted)' }}>
|
||||
We're building a proper docs site with guides for connecting, self-hosting, contributing,
|
||||
and the opencode Cloud. Want to help write docs?
|
||||
</p>
|
||||
<a
|
||||
href="https://github.com/dzianisv/opencode-mobile/discussions"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-primary text-sm"
|
||||
>
|
||||
Join the Discussion
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
129
website/app/globals.css
Normal file
@@ -0,0 +1,129 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--bg: #000000;
|
||||
--fg: #F1ECEC;
|
||||
--accent: #F1ECEC;
|
||||
--accent-hover: #ffffff;
|
||||
--muted: #8E8B8B;
|
||||
--card: #0A0A0A;
|
||||
--border: #1F1F1F;
|
||||
--green: #03B000;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--font-geist-sans), system-ui, -apple-system, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.btn-primary {
|
||||
@apply inline-flex items-center gap-2 px-6 py-3 rounded-lg font-semibold transition-all;
|
||||
background-color: var(--fg);
|
||||
color: var(--bg);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: #ffffff;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply inline-flex items-center gap-2 px-6 py-3 rounded-lg font-semibold transition-all;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: var(--card);
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
@apply rounded-xl p-6;
|
||||
}
|
||||
.card:hover {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.prose-site h2 {
|
||||
@apply text-2xl font-bold mt-10 mb-4;
|
||||
color: var(--fg);
|
||||
border-left: 4px solid var(--fg);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.prose-site h3 {
|
||||
@apply text-xl font-semibold mt-8 mb-3;
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.prose-site p {
|
||||
@apply mb-4 leading-relaxed;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prose-site ul {
|
||||
@apply mb-4 ml-6 space-y-2;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prose-site ul li {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
.prose-site a {
|
||||
color: var(--fg);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
|
||||
.prose-site a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.prose-site table {
|
||||
@apply w-full text-sm mb-6;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.prose-site th {
|
||||
@apply px-4 py-2 text-left font-semibold;
|
||||
background-color: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg);
|
||||
}
|
||||
|
||||
.prose-site td {
|
||||
@apply px-4 py-2 align-top;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.prose-site tr:nth-child(even) td {
|
||||
background-color: rgba(10, 10, 10, 0.5);
|
||||
}
|
||||
|
||||
/* Install command box */
|
||||
.install-box {
|
||||
@apply flex items-center gap-3 px-5 py-4 rounded-lg font-mono text-sm;
|
||||
background-color: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--fg);
|
||||
}
|
||||
}
|
||||
98
website/app/layout.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import { GeistMono } from 'geist/font/mono'
|
||||
import './globals.css'
|
||||
import Nav from '@/components/Nav'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL('https://opencode.agentlabs.cc'),
|
||||
title: {
|
||||
default: 'OpenCode Mobile — Your AI Coding Agent, On the Go',
|
||||
template: '%s | OpenCode Mobile',
|
||||
},
|
||||
description:
|
||||
'OpenCode Mobile is a free, open-source Android & iOS client for the opencode AI coding agent. Connect to your own server, stream diffs, and code from anywhere.',
|
||||
keywords: [
|
||||
'opencode mobile',
|
||||
'AI coding agent',
|
||||
'mobile coding',
|
||||
'self-hosted AI',
|
||||
'opencode client',
|
||||
'open source',
|
||||
'Android AI coding',
|
||||
],
|
||||
authors: [{ name: 'Vibe Technologies' }],
|
||||
creator: 'Vibe Technologies',
|
||||
publisher: 'Vibe Technologies',
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
locale: 'en_US',
|
||||
url: 'https://opencode.agentlabs.cc',
|
||||
siteName: 'OpenCode Mobile',
|
||||
title: 'OpenCode Mobile — Your AI Coding Agent, On the Go',
|
||||
description:
|
||||
'Free, open-source mobile client for opencode. Multi-connection, biometric unlock, streaming diffs — code from anywhere.',
|
||||
images: [
|
||||
{
|
||||
url: '/og.png',
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'OpenCode Mobile',
|
||||
},
|
||||
],
|
||||
},
|
||||
twitter: {
|
||||
card: 'summary_large_image',
|
||||
title: 'OpenCode Mobile — Your AI Coding Agent, On the Go',
|
||||
description:
|
||||
'Free, open-source mobile client for opencode. Multi-connection, biometric unlock, streaming diffs.',
|
||||
images: ['/og.png'],
|
||||
creator: '@vibebrowserapp',
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
},
|
||||
},
|
||||
alternates: {
|
||||
canonical: 'https://opencode.agentlabs.cc',
|
||||
},
|
||||
icons: {
|
||||
icon: [
|
||||
{ url: '/favicon.svg', type: 'image/svg+xml' },
|
||||
],
|
||||
shortcut: '/favicon.svg',
|
||||
},
|
||||
}
|
||||
|
||||
export const viewport = {
|
||||
width: 'device-width',
|
||||
initialScale: 1,
|
||||
maximumScale: 5,
|
||||
themeColor: '#000000',
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${GeistSans.variable} ${GeistMono.variable}`}>
|
||||
<body className="min-h-screen flex flex-col" style={{ fontFamily: 'var(--font-geist-sans), system-ui, sans-serif' }}>
|
||||
<Nav />
|
||||
<main className="flex-1">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
)
|
||||
}
|
||||
215
website/app/page.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import type { Metadata } from 'next'
|
||||
import Link from 'next/link'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'OpenCode Mobile — The open source AI coding agent, on the go',
|
||||
description:
|
||||
'Free, open-source Android & iOS client for the opencode AI coding agent. Connect to your own server, stream diffs, and code from anywhere.',
|
||||
alternates: { canonical: 'https://opencode.agentlabs.cc' },
|
||||
}
|
||||
|
||||
const features = [
|
||||
{
|
||||
bullet: '[*]',
|
||||
title: 'Multi-server',
|
||||
description: 'Switch between local, cloud, and team servers. Credentials stay on device.',
|
||||
},
|
||||
{
|
||||
bullet: '[*]',
|
||||
title: 'Streaming diffs',
|
||||
description: 'Watch AI changes in real time with syntax-highlighted diffs.',
|
||||
},
|
||||
{
|
||||
bullet: '[*]',
|
||||
title: 'Biometric unlock',
|
||||
description: 'Fingerprint or Face ID. No password typing.',
|
||||
},
|
||||
{
|
||||
bullet: '[*]',
|
||||
title: 'Any model',
|
||||
description: 'Claude, GPT, Gemini, local models — whatever your server runs.',
|
||||
},
|
||||
{
|
||||
bullet: '[*]',
|
||||
title: 'Session management',
|
||||
description: 'Create, resume, and switch between coding sessions.',
|
||||
},
|
||||
{
|
||||
bullet: '[*]',
|
||||
title: 'MIT licensed',
|
||||
description: 'Fully open source. Audit, fork, self-build. Diagnostics are opt-in.',
|
||||
},
|
||||
]
|
||||
|
||||
const screenshots = [
|
||||
{ file: '04', alt: 'Add an OpenCode server connection' },
|
||||
{ file: '03', alt: 'Start a streaming coding task' },
|
||||
{ file: '02', alt: 'Watch live tool activity' },
|
||||
{ file: '05', alt: 'Browse coding sessions' },
|
||||
{ file: '06', alt: 'Review an agent writing code' },
|
||||
{ file: '07', alt: 'See a completed coding task' },
|
||||
{ file: '01', alt: 'Choose a model and task settings' },
|
||||
]
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<>
|
||||
{/* Hero */}
|
||||
<section className="pt-24 pb-20 md:pt-36 md:pb-28">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6">
|
||||
<h1 className="text-4xl sm:text-5xl md:text-6xl font-bold leading-[1.1] tracking-tight mb-6" style={{ color: 'var(--fg)' }}>
|
||||
The open source AI coding agent,<br />
|
||||
<span style={{ color: 'var(--muted)' }}>on the go</span>
|
||||
</h1>
|
||||
|
||||
<p className="text-lg sm:text-xl leading-relaxed mb-10 max-w-2xl" style={{ color: 'var(--muted)' }}>
|
||||
Free mobile client for{' '}
|
||||
<a href="https://opencode.ai" target="_blank" rel="noopener noreferrer"
|
||||
className="underline underline-offset-3 decoration-1" style={{ color: 'var(--fg)' }}>
|
||||
opencode
|
||||
</a>
|
||||
. Connect to your own server, stream diffs in real time, and review AI changes from anywhere.
|
||||
</p>
|
||||
|
||||
{/* Install / Download */}
|
||||
<div className="flex flex-wrap gap-3 mb-4">
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=cc.agentlabs.opencode"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-primary text-sm"
|
||||
>
|
||||
Download for Android
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/dzianisv/opencode-mobile/releases"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary text-sm"
|
||||
>
|
||||
GitHub Releases
|
||||
</a>
|
||||
<a
|
||||
href="https://dzianisv.github.io/opencode-mobile/fdroid/repo"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary text-sm"
|
||||
>
|
||||
F-Droid
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<p className="text-xs" style={{ color: 'var(--muted)' }}>
|
||||
Android 8.0+ · iOS coming soon
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Approved Google Play screenshots, ordered from connection to completed work. */}
|
||||
<section className="pb-24">
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center" style={{ color: 'var(--fg)' }}>
|
||||
From connection to completed code
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-4 sm:gap-6">
|
||||
{screenshots.map((screenshot) => (
|
||||
<div key={screenshot.file} className="relative rounded-2xl overflow-hidden"
|
||||
style={{ border: '1px solid var(--border)' }}>
|
||||
<img
|
||||
src={`/screenshots/v0.4.7/${screenshot.file}.png`}
|
||||
alt={screenshot.alt}
|
||||
width={1280}
|
||||
height={2560}
|
||||
className="w-full h-auto object-contain"
|
||||
style={{ display: 'block' }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Showcase video */}
|
||||
<section className="pb-24">
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6">
|
||||
<h2 className="text-2xl font-bold mb-6 text-center" style={{ color: 'var(--fg)' }}>
|
||||
See it in action
|
||||
</h2>
|
||||
<div className="relative rounded-2xl overflow-hidden" style={{ border: '1px solid var(--border)' }}>
|
||||
<video
|
||||
src="/cua-showcase.mp4"
|
||||
controls
|
||||
muted
|
||||
playsInline
|
||||
poster="/screenshots/v0.4.7/03.png"
|
||||
style={{ width: '100%', display: 'block' }}
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Features — opencode.ai style bullet list */}
|
||||
<section className="py-20 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold mb-12" style={{ color: 'var(--fg)' }}>
|
||||
What you get
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-16 gap-y-8">
|
||||
{features.map((f) => (
|
||||
<div key={f.title} className="flex gap-4">
|
||||
<span className="text-sm font-mono shrink-0 mt-0.5" style={{ color: 'var(--muted)' }}>{f.bullet}</span>
|
||||
<div>
|
||||
<h3 className="font-semibold mb-1" style={{ color: 'var(--fg)' }}>{f.title}</h3>
|
||||
<p className="text-sm leading-relaxed" style={{ color: 'var(--muted)' }}>{f.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Open Source */}
|
||||
<section className="py-20 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6">
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center justify-between gap-8">
|
||||
<div>
|
||||
<h2 className="text-2xl sm:text-3xl font-bold mb-3" style={{ color: 'var(--fg)' }}>
|
||||
100% open source
|
||||
</h2>
|
||||
<p className="max-w-lg text-sm leading-relaxed" style={{ color: 'var(--muted)' }}>
|
||||
MIT-licensed. Audit the code, self-build, contribute features, or fork it.
|
||||
No telemetry without consent. No hidden servers. Your data stays yours.
|
||||
</p>
|
||||
</div>
|
||||
<a
|
||||
href="https://github.com/dzianisv/opencode-mobile"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn-secondary text-sm shrink-0"
|
||||
>
|
||||
View on GitHub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* CTA */}
|
||||
<section className="py-20 border-t" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 text-center">
|
||||
<h2 className="text-2xl sm:text-3xl font-bold mb-4" style={{ color: 'var(--fg)' }}>
|
||||
Join the beta
|
||||
</h2>
|
||||
<p className="text-sm mb-8 max-w-lg mx-auto" style={{ color: 'var(--muted)' }}>
|
||||
We need 20 testers to unlock the public Google Play release.
|
||||
Sign up and get early access today.
|
||||
</p>
|
||||
<Link href="/beta" className="btn-primary text-sm">
|
||||
Join the closed beta
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
61
website/app/privacy/page.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { Metadata } from 'next'
|
||||
import { readFileSync } from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Privacy Policy',
|
||||
description:
|
||||
'OpenCode Mobile privacy policy. We do not collect your code, prompts, or AI responses. Crash diagnostics via Sentry are opt-in only.',
|
||||
alternates: { canonical: 'https://opencode.agentlabs.cc/privacy' },
|
||||
openGraph: {
|
||||
title: 'Privacy Policy | OpenCode Mobile',
|
||||
description: 'OpenCode Mobile privacy policy — minimal data collection, opt-in crash reporting.',
|
||||
url: 'https://opencode.agentlabs.cc/privacy',
|
||||
},
|
||||
}
|
||||
|
||||
export default function PrivacyPage() {
|
||||
// Read the canonical privacy policy HTML generated by the Android distribution workflow.
|
||||
// This ensures the policy shown on the site is always in sync with app store submissions.
|
||||
const policyPath = path.join(
|
||||
process.cwd(),
|
||||
'../../../opencode-mobile/distribution/privacy-policy.html',
|
||||
)
|
||||
|
||||
let policyHtml = ''
|
||||
try {
|
||||
const raw = readFileSync(policyPath, 'utf8')
|
||||
// Extract just the <body> content so we can render it inside our layout
|
||||
const bodyMatch = raw.match(/<body[^>]*>([\s\S]*?)<\/body>/i)
|
||||
policyHtml = bodyMatch ? bodyMatch[1] : raw
|
||||
} catch {
|
||||
policyHtml = `<p style="color:#94A3B8">Privacy policy document not found. Please contact
|
||||
<a href="mailto:support@vibebrowser.app" style="color:#3B82F6">support@vibebrowser.app</a>.</p>`
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-16">
|
||||
{/* Override the inline HTML colors from the standalone file with dark theme */}
|
||||
<style>{`
|
||||
.privacy-content body { color: inherit; background: transparent; }
|
||||
.privacy-content header { border-bottom-color: #3B82F6; }
|
||||
.privacy-content header h1 { color: #F1F5F9; }
|
||||
.privacy-content .meta { color: #94A3B8; }
|
||||
.privacy-content h2 { color: #F1F5F9; border-left-color: #3B82F6; }
|
||||
.privacy-content p, .privacy-content li { color: #CBD5E1; }
|
||||
.privacy-content a { color: #3B82F6; }
|
||||
.privacy-content .highlight-box { background: rgba(59,130,246,0.1); border-color: rgba(59,130,246,0.4); }
|
||||
.privacy-content .highlight-box strong { color: #93C5FD; }
|
||||
.privacy-content th { background: #1E293B; border-color: #334155; color: #F1F5F9; }
|
||||
.privacy-content td { border-color: #334155; color: #CBD5E1; }
|
||||
.privacy-content tr:nth-child(even) td { background: rgba(30,41,59,0.5); }
|
||||
.privacy-content footer { border-top-color: #334155; color: #64748B; }
|
||||
.privacy-content code { color: #94A3B8; font-size: 0.85em; }
|
||||
`}</style>
|
||||
<div
|
||||
className="privacy-content"
|
||||
dangerouslySetInnerHTML={{ __html: policyHtml }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
11
website/app/robots.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
},
|
||||
sitemap: 'https://opencode.agentlabs.cc/sitemap.xml',
|
||||
}
|
||||
}
|
||||
39
website/app/sitemap.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const base = 'https://opencode.agentlabs.cc'
|
||||
const now = new Date()
|
||||
|
||||
return [
|
||||
{
|
||||
url: base,
|
||||
lastModified: now,
|
||||
changeFrequency: 'weekly',
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: `${base}/privacy`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${base}/terms`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: `${base}/support`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.7,
|
||||
},
|
||||
{
|
||||
url: `${base}/docs`,
|
||||
lastModified: now,
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
]
|
||||
}
|
||||
121
website/app/support/page.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Support',
|
||||
description:
|
||||
'Get help with OpenCode Mobile. Contact us, browse GitHub Issues, and read the FAQ.',
|
||||
alternates: { canonical: 'https://opencode.agentlabs.cc/support' },
|
||||
openGraph: {
|
||||
title: 'Support | OpenCode Mobile',
|
||||
description: 'Contact, FAQ, and GitHub Issues for OpenCode Mobile.',
|
||||
url: 'https://opencode.agentlabs.cc/support',
|
||||
},
|
||||
}
|
||||
|
||||
const faqs = [
|
||||
{
|
||||
q: 'How do I connect the app to my opencode server?',
|
||||
a: `Open the app, tap the "+" button on the connections screen, and enter your server URL (e.g., https://your-server.example.com or http://192.168.1.x:3000 for local). The app connects over HTTP/HTTPS to the standard opencode REST/WebSocket API. For remote access, we recommend running your server behind Tailscale or a reverse proxy with TLS.`,
|
||||
},
|
||||
{
|
||||
q: 'Is my code sent to Vibe Technologies?',
|
||||
a: `No. All AI traffic — your prompts, code, diffs, and AI responses — travels directly between the app and your own opencode server. VIBE TECHNOLOGIES, LLC never sees this data. The only optional data we receive is anonymous crash diagnostics via Sentry, and only if you opt in at first launch.`,
|
||||
},
|
||||
{
|
||||
q: 'Can I self-host the backend?',
|
||||
a: `Yes — that's the primary use case. OpenCode Mobile is a client for the open-source opencode server (github.com/sst/opencode). You run the server wherever you like: your laptop, a VPS, a home server, or a cloud VM. The app simply connects to whatever URL you configure.`,
|
||||
},
|
||||
{
|
||||
q: 'Is the app really free? What\'s the catch?',
|
||||
a: `The app is free and MIT-licensed with no catch. We pay for development out of Vibe Technologies revenue. In the future we plan to offer "opencode Cloud" — a managed, always-on backend for teams who don't want to run their own server. That will be a paid product. The mobile app itself will remain free and open source.`,
|
||||
},
|
||||
{
|
||||
q: 'The app crashes or I see an error. What should I do?',
|
||||
a: `Please open a GitHub Issue at github.com/dzianisv/opencode-mobile/issues with: your device model, Android/iOS version, app version (Settings → About), and a description of what happened. If you opted in to crash reporting, we may already have the stack trace — your issue report helps us match it to the right bug.`,
|
||||
},
|
||||
]
|
||||
|
||||
export default function SupportPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-16">
|
||||
<h1 className="text-3xl font-bold mb-4" style={{ color: 'var(--fg)' }}>
|
||||
Support
|
||||
</h1>
|
||||
<p className="text-lg mb-12" style={{ color: 'var(--muted)' }}>
|
||||
We're a small team, but we try to respond within a few business days.
|
||||
</p>
|
||||
|
||||
{/* Contact cards */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5 mb-16">
|
||||
<a
|
||||
href="mailto:support@vibebrowser.app"
|
||||
className="card flex items-start gap-4 hover:border-blue-500/60 transition-colors"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<div className="shrink-0 w-10 h-10 rounded-lg flex items-center justify-center"
|
||||
style={{ backgroundColor: 'rgba(59,130,246,0.15)', color: 'var(--accent)' }}>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
|
||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold mb-1" style={{ color: 'var(--fg)' }}>Email Support</p>
|
||||
<p className="text-sm" style={{ color: 'var(--muted)' }}>support@vibebrowser.app</p>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://github.com/dzianisv/opencode-mobile/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="card flex items-start gap-4 hover:border-blue-500/60 transition-colors"
|
||||
style={{ textDecoration: 'none' }}
|
||||
>
|
||||
<div className="shrink-0 w-10 h-10 rounded-lg flex items-center justify-center"
|
||||
style={{ backgroundColor: 'rgba(59,130,246,0.15)', color: 'var(--accent)' }}>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path fillRule="evenodd" clipRule="evenodd"
|
||||
d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold mb-1" style={{ color: 'var(--fg)' }}>GitHub Issues</p>
|
||||
<p className="text-sm" style={{ color: 'var(--muted)' }}>Bug reports & feature requests</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* FAQ */}
|
||||
<h2 className="text-2xl font-bold mb-6" style={{ color: 'var(--fg)', borderLeft: '4px solid var(--accent)', paddingLeft: 12 }}>
|
||||
Frequently Asked Questions
|
||||
</h2>
|
||||
|
||||
<div className="space-y-5">
|
||||
{faqs.map((faq, i) => (
|
||||
<div key={i} className="card">
|
||||
<h3 className="font-semibold mb-3" style={{ color: 'var(--fg)' }}>
|
||||
{faq.q}
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed" style={{ color: '#CBD5E1' }}>
|
||||
{faq.a}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-12 p-5 rounded-xl text-sm" style={{ background: 'rgba(59,130,246,0.08)', border: '1px solid rgba(59,130,246,0.25)', color: 'var(--muted)' }}>
|
||||
Still stuck? Open a{' '}
|
||||
<a href="https://github.com/dzianisv/opencode-mobile/issues/new" target="_blank"
|
||||
rel="noopener noreferrer" style={{ color: 'var(--accent)' }}>
|
||||
GitHub Issue
|
||||
</a>{' '}
|
||||
or email{' '}
|
||||
<a href="mailto:support@vibebrowser.app" style={{ color: 'var(--accent)' }}>
|
||||
support@vibebrowser.app
|
||||
</a>.
|
||||
Include your device, OS version, and app version for fastest resolution.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
199
website/app/terms/page.tsx
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Terms of Service',
|
||||
description:
|
||||
'OpenCode Mobile terms of service. MIT-licensed open-source software provided as-is, no warranty. Governing law: Washington State.',
|
||||
alternates: { canonical: 'https://opencode.agentlabs.cc/terms' },
|
||||
openGraph: {
|
||||
title: 'Terms of Service | OpenCode Mobile',
|
||||
description: 'OpenCode Mobile terms of service — MIT-licensed, provided as-is.',
|
||||
url: 'https://opencode.agentlabs.cc/terms',
|
||||
},
|
||||
}
|
||||
|
||||
export default function TermsPage() {
|
||||
return (
|
||||
<div className="max-w-3xl mx-auto px-4 sm:px-6 py-16">
|
||||
<header className="border-b pb-6 mb-10" style={{ borderColor: '#3B82F6' }}>
|
||||
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--fg)' }}>
|
||||
OpenCode Mobile — Terms of Service
|
||||
</h1>
|
||||
<p className="text-sm" style={{ color: 'var(--muted)' }}>
|
||||
Effective date: 2026-05-24 |
|
||||
Operator: VIBE TECHNOLOGIES, LLC |
|
||||
App: OpenCode Mobile (<code className="text-xs px-1 rounded" style={{ background: 'var(--card)' }}>ai.opencode.mobile</code>)
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div className="prose-site">
|
||||
<div className="rounded-xl p-5 mb-8" style={{ background: 'rgba(59,130,246,0.1)', border: '1px solid rgba(59,130,246,0.3)' }}>
|
||||
<p className="!mb-0">
|
||||
<strong style={{ color: '#93C5FD' }}>Summary:</strong> OpenCode Mobile is free,
|
||||
open-source (MIT) software. It is provided as-is. You run it against your own opencode
|
||||
server. We owe you no support on the free tier, though we genuinely try to help.
|
||||
Governing law is Washington State, USA.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<h2>1. Acceptance of Terms</h2>
|
||||
<p>
|
||||
By downloading, installing, or using OpenCode Mobile (the “App”), you agree to
|
||||
these Terms of Service (“Terms”). If you do not agree, do not use the App.
|
||||
</p>
|
||||
<p>
|
||||
These Terms apply to all versions of the App, including builds distributed via Google Play,
|
||||
F-Droid, Apple App Store, or directly from the GitHub repository.
|
||||
</p>
|
||||
|
||||
<h2>2. Open-Source License</h2>
|
||||
<p>
|
||||
The App is released under the{' '}
|
||||
<a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer">MIT License</a>.
|
||||
Source code is available at{' '}
|
||||
<a href="https://github.com/dzianisv/opencode-mobile" target="_blank" rel="noopener noreferrer">
|
||||
github.com/dzianisv/opencode-mobile
|
||||
</a>.
|
||||
</p>
|
||||
<p>
|
||||
The MIT License grants you broad rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the software, subject to the license's attribution
|
||||
requirement. These Terms supplement, but do not replace, the MIT License.
|
||||
</p>
|
||||
|
||||
<h2>3. Description of Service</h2>
|
||||
<p>
|
||||
OpenCode Mobile is a mobile client application that connects to a self-hosted or
|
||||
third-party-hosted opencode server. The App does not itself provide AI services; all AI
|
||||
computation occurs on the server you configure. VIBE TECHNOLOGIES, LLC is not a party to
|
||||
the communication between the App and your server.
|
||||
</p>
|
||||
|
||||
<h2>4. Your Responsibilities</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<strong>Your server:</strong> You are solely responsible for the opencode server you
|
||||
connect to, including its security, uptime, costs, and compliance with applicable laws
|
||||
and the terms of any AI provider you use on that server.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Your content:</strong> You are solely responsible for any code, prompts, or other
|
||||
content you submit through the App to your server.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Lawful use:</strong> You agree to use the App only for lawful purposes and in
|
||||
accordance with applicable laws and regulations.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Age:</strong> You must be at least 18 years old to use the App, as it is a
|
||||
developer tool not designed for minors.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Account security:</strong> You are responsible for safeguarding any credentials,
|
||||
tokens, or API keys you configure in the App.
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2>5. No Warranty</h2>
|
||||
<p>
|
||||
THE APP IS PROVIDED “AS IS” AND “AS AVAILABLE,” WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR
|
||||
A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. VIBE TECHNOLOGIES, LLC DOES NOT WARRANT THAT
|
||||
THE APP WILL BE ERROR-FREE, UNINTERRUPTED, SECURE, OR MEET YOUR REQUIREMENTS.
|
||||
</p>
|
||||
<p>
|
||||
This disclaimer is consistent with the MIT License under which the App's source code is
|
||||
released.
|
||||
</p>
|
||||
|
||||
<h2>6. Limitation of Liability</h2>
|
||||
<p>
|
||||
TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL VIBE TECHNOLOGIES,
|
||||
LLC, ITS OFFICERS, DIRECTORS, EMPLOYEES, OR AGENTS BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
||||
SPECIAL, CONSEQUENTIAL, PUNITIVE, OR EXEMPLARY DAMAGES, INCLUDING LOSS OF PROFITS, DATA,
|
||||
BUSINESS, OR GOODWILL, ARISING OUT OF OR IN CONNECTION WITH YOUR USE OF THE APP, EVEN IF
|
||||
ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
</p>
|
||||
<p>
|
||||
IN JURISDICTIONS THAT DO NOT ALLOW THE EXCLUSION OR LIMITATION OF CERTAIN DAMAGES, OUR
|
||||
LIABILITY IS LIMITED TO THE GREATEST EXTENT PERMITTED BY LAW.
|
||||
</p>
|
||||
|
||||
<h2>7. No Support Obligation (Free Tier)</h2>
|
||||
<p>
|
||||
VIBE TECHNOLOGIES, LLC provides OpenCode Mobile free of charge and has no obligation to
|
||||
provide technical support, maintenance, updates, or bug fixes. We genuinely try to respond
|
||||
to issues on{' '}
|
||||
<a href="https://github.com/dzianisv/opencode-mobile/issues" target="_blank" rel="noopener noreferrer">
|
||||
GitHub Issues
|
||||
</a>{' '}
|
||||
and via{' '}
|
||||
<a href="mailto:support@vibebrowser.app">support@vibebrowser.app</a>, but we make no
|
||||
guarantees about response times or resolution.
|
||||
</p>
|
||||
<p>
|
||||
Future paid “opencode Cloud” plans may include defined support terms; those plans will
|
||||
have separate agreements.
|
||||
</p>
|
||||
|
||||
<h2>8. Updates and Changes to the App</h2>
|
||||
<p>
|
||||
We may release updates, patches, or new versions of the App at any time or discontinue the
|
||||
App without notice. We are not obligated to maintain backwards compatibility or provide
|
||||
migration paths.
|
||||
</p>
|
||||
|
||||
<h2>9. Third-Party Services</h2>
|
||||
<p>
|
||||
The App may integrate with or link to third-party services (e.g., your opencode server,
|
||||
AI providers, Sentry for crash reporting). These third parties have their own terms and
|
||||
privacy policies. VIBE TECHNOLOGIES, LLC is not responsible for third-party services.
|
||||
</p>
|
||||
|
||||
<h2>10. Intellectual Property</h2>
|
||||
<p>
|
||||
The App's source code is MIT-licensed (see Section 2). The “OpenCode Mobile” name, the
|
||||
Vibe Technologies name, and associated logos are trademarks of VIBE TECHNOLOGIES, LLC and
|
||||
may not be used without permission except as permitted by applicable law.
|
||||
</p>
|
||||
|
||||
<h2>11. Governing Law and Dispute Resolution</h2>
|
||||
<p>
|
||||
These Terms are governed by and construed in accordance with the laws of the State of
|
||||
Washington, USA, without regard to its conflict-of-law provisions. Any dispute arising out
|
||||
of or relating to these Terms or the App shall be subject to the exclusive jurisdiction of
|
||||
the state and federal courts located in King County, Washington.
|
||||
</p>
|
||||
<p>
|
||||
If you are a consumer in a jurisdiction with mandatory consumer protection laws, nothing in
|
||||
these Terms limits rights you are entitled to by law.
|
||||
</p>
|
||||
|
||||
<h2>12. Changes to These Terms</h2>
|
||||
<p>
|
||||
We may update these Terms from time to time. The most current version will always be
|
||||
available at{' '}
|
||||
<a href="https://opencode.agentlabs.cc/terms">opencode.agentlabs.cc/terms</a>.
|
||||
Continued use of the App after changes constitutes your acceptance of the new Terms.
|
||||
We will update the effective date at the top when changes are made.
|
||||
</p>
|
||||
|
||||
<h2>13. Severability</h2>
|
||||
<p>
|
||||
If any provision of these Terms is found to be unenforceable, the remaining provisions
|
||||
will continue in full force and effect.
|
||||
</p>
|
||||
|
||||
<h2>14. Contact</h2>
|
||||
<p>
|
||||
VIBE TECHNOLOGIES, LLC<br />
|
||||
519 S Henderson St<br />
|
||||
Seattle, WA 98108-4522<br />
|
||||
USA<br />
|
||||
Email:{' '}
|
||||
<a href="mailto:support@vibebrowser.app">support@vibebrowser.app</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
42
website/components/Footer.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer className="border-t mt-20" style={{ borderColor: 'var(--border)' }}>
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 py-10">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-6">
|
||||
{/* Links */}
|
||||
<div className="flex flex-wrap items-center gap-x-6 gap-y-2 text-sm" style={{ color: 'var(--muted)' }}>
|
||||
<a
|
||||
href="https://github.com/dzianisv/opencode-mobile"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-colors"
|
||||
>
|
||||
GitHub
|
||||
</a>
|
||||
<a
|
||||
href="https://opencode.ai"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:opacity-80 transition-colors"
|
||||
>
|
||||
Docs
|
||||
</a>
|
||||
<Link href="/privacy" className="hover:opacity-80 transition-colors">
|
||||
Privacy
|
||||
</Link>
|
||||
<Link href="/terms" className="hover:opacity-80 transition-colors">
|
||||
Terms
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Legal */}
|
||||
<p className="text-xs" style={{ color: 'var(--muted)' }}>
|
||||
© 2026 VIBE TECHNOLOGIES, LLC
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
89
website/components/Nav.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
'use client'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
|
||||
export default function Nav() {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="sticky top-0 z-50 border-b"
|
||||
style={{ backgroundColor: 'rgba(0, 0, 0, 0.9)', borderColor: 'var(--border)', backdropFilter: 'blur(12px)' }}
|
||||
>
|
||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 flex items-center justify-between h-16">
|
||||
<Link href="/" className="flex items-center gap-2 font-bold text-lg tracking-tight" style={{ color: 'var(--fg)' }}>
|
||||
opencode<span className="font-normal" style={{ color: 'var(--muted)' }}>/mobile</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop links */}
|
||||
<div className="hidden md:flex items-center gap-6">
|
||||
{[
|
||||
{ href: 'https://opencode.ai', label: 'Docs', external: true },
|
||||
{ href: 'https://github.com/dzianisv/opencode-mobile', label: 'GitHub', external: true },
|
||||
{ href: '/beta', label: 'Beta' },
|
||||
].map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm transition-colors hover:opacity-80"
|
||||
style={{ color: 'var(--muted)' }}
|
||||
{...('external' in link ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
<a
|
||||
href="https://play.google.com/store/apps/details?id=cc.agentlabs.opencode"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm font-medium px-4 py-2 rounded-lg transition-all"
|
||||
style={{ backgroundColor: 'var(--fg)', color: 'var(--bg)' }}
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{/* Mobile hamburger */}
|
||||
<button
|
||||
className="md:hidden p-2 rounded-lg"
|
||||
style={{ color: 'var(--muted)' }}
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-label="Toggle menu"
|
||||
>
|
||||
{open ? (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
{open && (
|
||||
<div className="md:hidden border-t px-4 py-4 space-y-3" style={{ borderColor: 'var(--border)', backgroundColor: 'var(--bg)' }}>
|
||||
{[
|
||||
{ href: 'https://opencode.ai', label: 'Docs', external: true },
|
||||
{ href: 'https://github.com/dzianisv/opencode-mobile', label: 'GitHub', external: true },
|
||||
{ href: '/beta', label: 'Join Beta' },
|
||||
{ href: 'https://play.google.com/store/apps/details?id=cc.agentlabs.opencode', label: 'Download', external: true },
|
||||
].map((link) => (
|
||||
<Link
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="block text-sm py-2 transition-colors hover:opacity-80"
|
||||
style={{ color: 'var(--muted)' }}
|
||||
onClick={() => setOpen(false)}
|
||||
{...('external' in link ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
|
||||
>
|
||||
{link.label}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
6
website/next-env.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
12
website/next.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { NextConfig } from 'next'
|
||||
|
||||
const root = process.cwd()
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
// Static export disabled — using Vercel SSR so robots.txt and sitemap work normally
|
||||
turbopack: {
|
||||
root,
|
||||
},
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
2358
website/package-lock.json
generated
Normal file
28
website/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "opencode-mobile-site",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"prebuild": "node scripts/verify-screenshots.mjs",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@vercel/otel": "^2.1.3",
|
||||
"geist": "^1.3.1",
|
||||
"next": "^16.2.6",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
9
website/postcss.config.mjs
Normal file
@@ -0,0 +1,9 @@
|
||||
/** @type {import('postcss').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
BIN
website/public/cua-showcase.mp4
Normal file
4
website/public/favicon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<rect width="32" height="32" rx="8" fill="#0F172A"/>
|
||||
<text x="16" y="22" font-family="monospace" font-size="16" font-weight="bold" fill="#3B82F6" text-anchor="middle"></></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
BIN
website/public/screenshots/v0.4.7/01.png
Normal file
|
After Width: | Height: | Size: 648 KiB |
BIN
website/public/screenshots/v0.4.7/02.png
Normal file
|
After Width: | Height: | Size: 479 KiB |
BIN
website/public/screenshots/v0.4.7/03.png
Normal file
|
After Width: | Height: | Size: 338 KiB |
BIN
website/public/screenshots/v0.4.7/04.png
Normal file
|
After Width: | Height: | Size: 369 KiB |
BIN
website/public/screenshots/v0.4.7/05.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
BIN
website/public/screenshots/v0.4.7/06.png
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
website/public/screenshots/v0.4.7/07.png
Normal file
|
After Width: | Height: | Size: 584 KiB |
80
website/public/screenshots/v0.4.7/manifest.json
Normal file
@@ -0,0 +1,80 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"source": {
|
||||
"platform": "Google Play",
|
||||
"listing": "https://play.google.com/store/apps/details?id=cc.agentlabs.opencode&hl=en_US",
|
||||
"packageName": "cc.agentlabs.opencode",
|
||||
"retrievedAt": "2026-07-14",
|
||||
"ownership": "OpenCode Mobile listing assets"
|
||||
},
|
||||
"assetPolicy": {
|
||||
"purpose": "Canonical reference assets for the OpenCode Mobile website and future store-listing updates.",
|
||||
"doNotModify": "Do not crop, add marketing copy, or otherwise alter these files in place. Create a separately named derivative for a channel-specific composition.",
|
||||
"websiteSource": "Copy selected originals into website/public/screenshots/ during the website deployment change."
|
||||
},
|
||||
"screenshots": [
|
||||
{
|
||||
"file": "01.png",
|
||||
"sourceUrl": "https://play-lh.googleusercontent.com/0TndhVWCZHGC0UpZNEWBJfhAsRI1NPeg5aevMqdTTEmCQ1zE-HPwMVt0eGxxW2ljPAhGynebJ346MKP2JYAf9YU",
|
||||
"sha256": "9c2c1648458d52e9fdc6f9b45777d0fbf20f07fa3369e7aa747b708c4c7a5102",
|
||||
"width": 1280,
|
||||
"height": 2560,
|
||||
"feature": "Model and settings selection",
|
||||
"websitePlacement": "gallery-07"
|
||||
},
|
||||
{
|
||||
"file": "02.png",
|
||||
"sourceUrl": "https://play-lh.googleusercontent.com/l9Q1182qqZIJ6fSoXUMmlctaDMAt6RJbgZYVUA4akx2Fd7UKQ5R8MH0jbWGFI0_RVf-vBQMiTTixrTANWJKWpw",
|
||||
"sha256": "7928699bd4ea3c64d22bce60d05854d09699bf74b23f8390e62ed900fdf0d1e8",
|
||||
"width": 1280,
|
||||
"height": 2560,
|
||||
"feature": "Live tool activity",
|
||||
"websitePlacement": "gallery-04"
|
||||
},
|
||||
{
|
||||
"file": "03.png",
|
||||
"sourceUrl": "https://play-lh.googleusercontent.com/m1EkbhW1W17NNt1wyMOl7pGpdRgzwbULsxkrZ-KVuVlppTnYfN6dmXWmIoy2a8obp3uY6yg3LCuDN5_obHopTiQ",
|
||||
"sha256": "8987a963d708563f5ff2c0f1c127d852bcbd3759cd9eef2b462cee4d0d8e8658",
|
||||
"width": 1280,
|
||||
"height": 2560,
|
||||
"feature": "Streaming task conversation",
|
||||
"websitePlacement": "hero-02"
|
||||
},
|
||||
{
|
||||
"file": "04.png",
|
||||
"sourceUrl": "https://play-lh.googleusercontent.com/n3CtQbxCaV9V715VC9Dk5LYqP-ufQr2TA9Jsm-iKowTgN562_SWX663a_FpS1iGhAIFJ1QCSRAz5jtEinuosig",
|
||||
"sha256": "d7af61652e9a1521457e87c54c1702a1318e811201a46c40af9b59b85e09e2a2",
|
||||
"width": 1280,
|
||||
"height": 2560,
|
||||
"feature": "Connection setup",
|
||||
"websitePlacement": "hero-01"
|
||||
},
|
||||
{
|
||||
"file": "05.png",
|
||||
"sourceUrl": "https://play-lh.googleusercontent.com/nkYKYJzFD3Q3hVHpsUUhbd6ICirWLUYMcuxTK3Tg_QiCA3YPI_8PZiepI98BY9gddaWzF3xBZEDHmEo15dfyBQ",
|
||||
"sha256": "6acbbd87f4f48e5cf53deddd562c2099f8bab892bbd8f0bb00aac24e6f63ed85",
|
||||
"width": 1280,
|
||||
"height": 2560,
|
||||
"feature": "Session list",
|
||||
"websitePlacement": "gallery-02"
|
||||
},
|
||||
{
|
||||
"file": "06.png",
|
||||
"sourceUrl": "https://play-lh.googleusercontent.com/npsXfCkwCWEqJ9o56ZCENBTTrvefgnA54fGQLc6L1USc6wp9oFsfC_m_orBZ_hsZsE7pUuCDl51QSvkLy7w7m8c",
|
||||
"sha256": "4d68454a602be5546085ceea7f261aadb332973721bde3123d01304dc95d1651",
|
||||
"width": 1280,
|
||||
"height": 2560,
|
||||
"feature": "Agent writes code",
|
||||
"websitePlacement": "gallery-05"
|
||||
},
|
||||
{
|
||||
"file": "07.png",
|
||||
"sourceUrl": "https://play-lh.googleusercontent.com/zXUoTydFFH3U-Hc1hemnXFaxluRrWdDuRweatkuc5zRQQH1f806lN7ZY_go23OMxs5E5-7rD5JOZHlI-M9ACcQ",
|
||||
"sha256": "02be91c4ab35e5ab30f7e310ea5bcf98e0485b14b7d3bbe7f161a38e1642c427",
|
||||
"width": 1280,
|
||||
"height": 2560,
|
||||
"feature": "Completed coding task",
|
||||
"websitePlacement": "gallery-06"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
website/scripts/verify-screenshots.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { createHash } from "node:crypto"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { resolve } from "node:path"
|
||||
|
||||
const root = resolve(import.meta.dirname, "..")
|
||||
const directory = resolve(root, "public/screenshots/v0.4.7")
|
||||
const manifest = JSON.parse(await readFile(resolve(directory, "manifest.json"), "utf8"))
|
||||
|
||||
for (const screenshot of manifest.screenshots) {
|
||||
const file = resolve(directory, screenshot.file)
|
||||
const contents = await readFile(file)
|
||||
const hash = createHash("sha256").update(contents).digest("hex")
|
||||
if (hash !== screenshot.sha256) {
|
||||
throw new Error(`${screenshot.file} does not match its approved Play checksum`)
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Verified ${manifest.screenshots.length} approved screenshots.`)
|
||||
24
website/tailwind.config.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
content: [
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
slate: {
|
||||
950: '#0F172A',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['var(--font-geist-sans)', 'system-ui', 'sans-serif'],
|
||||
mono: ['var(--font-geist-mono)', 'monospace'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
export default config
|
||||
41
website/tsconfig.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||