feat: Build Early Access page with backend email capture
Combined page: countdown timer (Feb 28), email capture with server count segmentation (wired to POST /api/early-access), Founding Admin Program (25 slots), demo dashboard preview placeholders, roadmap voting, and launch timeline. Backend: Axum handler, migration for early_access_signups table with email + server_count + created_at. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -164,6 +164,11 @@ const routes: RouteRecordRaw[] = [
|
||||
name: 'roadmap',
|
||||
component: () => import('@/views/marketing/RoadmapView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'early-access',
|
||||
name: 'early-access',
|
||||
component: () => import('@/views/marketing/EarlyAccessView.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
|
||||
366
frontend/src/views/marketing/EarlyAccessView.vue
Normal file
366
frontend/src/views/marketing/EarlyAccessView.vue
Normal file
@@ -0,0 +1,366 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { Shield, Users, Star, MessageCircle, Clock, ChevronRight, Check, Zap, Terminal, RefreshCw, LayoutDashboard } from 'lucide-vue-next'
|
||||
|
||||
// ---------- Countdown ----------
|
||||
const targetDate = new Date('2026-02-28T12:00:00-05:00')
|
||||
const now = ref(Date.now())
|
||||
let timer: ReturnType<typeof setInterval>
|
||||
|
||||
const countdown = computed(() => {
|
||||
const diff = Math.max(0, targetDate.getTime() - now.value)
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000)
|
||||
return { days, hours, minutes, seconds }
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
timer = setInterval(() => { now.value = Date.now() }, 1000)
|
||||
})
|
||||
onUnmounted(() => clearInterval(timer))
|
||||
|
||||
// ---------- Email capture ----------
|
||||
const email = ref('')
|
||||
const serverCount = ref('')
|
||||
const submitting = ref(false)
|
||||
const submitted = ref(false)
|
||||
const errorMsg = ref('')
|
||||
|
||||
async function handleSubmit() {
|
||||
if (!email.value || !serverCount.value) return
|
||||
errorMsg.value = ''
|
||||
submitting.value = true
|
||||
try {
|
||||
const res = await fetch('/api/early-access', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: email.value,
|
||||
server_count: serverCount.value,
|
||||
}),
|
||||
})
|
||||
if (!res.ok) {
|
||||
const data = await res.json().catch(() => ({ message: 'Something went wrong' }))
|
||||
throw new Error(data.message || `HTTP ${res.status}`)
|
||||
}
|
||||
submitted.value = true
|
||||
} catch (err: unknown) {
|
||||
errorMsg.value = err instanceof Error ? err.message : 'Something went wrong'
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Demo panels ----------
|
||||
const panels = [
|
||||
{ label: 'Dashboard', icon: LayoutDashboard, desc: 'Server overview, player count, uptime, and alerts at a glance.' },
|
||||
{ label: 'Wipe Scheduler', icon: RefreshCw, desc: 'Visual wipe timeline with pre-wipe backup, map rotation, and health verification.' },
|
||||
{ label: 'Plugin Config', icon: Zap, desc: 'Edit plugin settings from your browser. No JSON. No SFTP.' },
|
||||
{ label: 'Player Management', icon: Users, desc: 'Online players, session tracking, kick/ban controls, and playtime history.' },
|
||||
{ label: 'Console', icon: Terminal, desc: 'Real-time RCON console with timestamped, color-coded output.' },
|
||||
]
|
||||
|
||||
// ---------- Roadmap voting ----------
|
||||
interface VoteItem {
|
||||
id: string
|
||||
label: string
|
||||
votes: number
|
||||
voted: boolean
|
||||
}
|
||||
|
||||
const voteItems = ref<VoteItem[]>([
|
||||
{ id: 'analytics', label: 'Analytics & Retention Insights', votes: 47, voted: false },
|
||||
{ id: 'webstore', label: 'Integrated Webstore', votes: 38, voted: false },
|
||||
{ id: 'modules', label: 'Module Marketplace', votes: 31, voted: false },
|
||||
{ id: 'discord', label: 'Discord Bot Integration', votes: 28, voted: false },
|
||||
{ id: 'hosting', label: 'Hosting Provider API', votes: 19, voted: false },
|
||||
])
|
||||
|
||||
function vote(item: VoteItem) {
|
||||
if (item.voted) return
|
||||
item.votes++
|
||||
item.voted = true
|
||||
}
|
||||
|
||||
const totalVotes = computed(() => voteItems.value.reduce((sum, i) => sum + i.votes, 0))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Hero -->
|
||||
<section class="relative overflow-hidden">
|
||||
<div class="max-w-4xl mx-auto px-6 pt-20 pb-16 text-center">
|
||||
<span class="inline-block px-4 py-1.5 bg-oxide-500/10 border border-oxide-500/20 rounded-full text-oxide-400 text-sm font-medium mb-6">
|
||||
Early Access Opening Soon
|
||||
</span>
|
||||
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4 tracking-tight">
|
||||
Wipe Night Is About to<br />
|
||||
<span class="text-oxide-500">Get Easier.</span>
|
||||
</h1>
|
||||
<p class="text-lg text-neutral-400 max-w-xl mx-auto mb-10">
|
||||
Corrosion is entering limited early access. Install once. Automate everything. Never SSH again.
|
||||
</p>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<a href="#join" class="px-8 py-3.5 bg-oxide-600 hover:bg-oxide-700 text-white font-semibold rounded-lg transition-colors">
|
||||
Join Early Access
|
||||
</a>
|
||||
<a href="#demo" class="px-8 py-3.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 font-semibold rounded-lg border border-neutral-700 transition-colors">
|
||||
View Demo Architecture
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] bg-oxide-500/8 rounded-full blur-3xl pointer-events-none" />
|
||||
</section>
|
||||
|
||||
<!-- Countdown -->
|
||||
<section class="py-12 border-t border-neutral-800">
|
||||
<div class="max-w-3xl mx-auto px-6 text-center">
|
||||
<p class="text-sm text-neutral-500 uppercase tracking-wider mb-6">Early Access Opens In</p>
|
||||
<div class="flex items-center justify-center gap-4 md:gap-6">
|
||||
<div class="text-center">
|
||||
<div class="w-20 h-20 bg-neutral-900 border border-neutral-800 rounded-xl flex items-center justify-center">
|
||||
<span class="text-3xl font-bold text-oxide-400 tabular-nums">{{ countdown.days }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 mt-2">Days</p>
|
||||
</div>
|
||||
<span class="text-2xl text-neutral-700 font-light">:</span>
|
||||
<div class="text-center">
|
||||
<div class="w-20 h-20 bg-neutral-900 border border-neutral-800 rounded-xl flex items-center justify-center">
|
||||
<span class="text-3xl font-bold text-oxide-400 tabular-nums">{{ String(countdown.hours).padStart(2, '0') }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 mt-2">Hours</p>
|
||||
</div>
|
||||
<span class="text-2xl text-neutral-700 font-light">:</span>
|
||||
<div class="text-center">
|
||||
<div class="w-20 h-20 bg-neutral-900 border border-neutral-800 rounded-xl flex items-center justify-center">
|
||||
<span class="text-3xl font-bold text-neutral-200 tabular-nums">{{ String(countdown.minutes).padStart(2, '0') }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 mt-2">Minutes</p>
|
||||
</div>
|
||||
<span class="text-2xl text-neutral-700 font-light">:</span>
|
||||
<div class="text-center">
|
||||
<div class="w-20 h-20 bg-neutral-900 border border-neutral-800 rounded-xl flex items-center justify-center">
|
||||
<span class="text-3xl font-bold text-neutral-200 tabular-nums">{{ String(countdown.seconds).padStart(2, '0') }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-neutral-500 mt-2">Seconds</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- What Early Access Means -->
|
||||
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800">
|
||||
<div class="max-w-4xl mx-auto px-6">
|
||||
<h2 class="text-2xl font-bold text-neutral-100 mb-8 text-center">What Early Access Means</h2>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
||||
<Shield class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
||||
<p class="text-sm font-medium text-neutral-200">Limited Founding Licenses</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">25–50 spots</p>
|
||||
</div>
|
||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
||||
<MessageCircle class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
||||
<p class="text-sm font-medium text-neutral-200">Direct Founder Discord</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">Private channel access</p>
|
||||
</div>
|
||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
||||
<Star class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
||||
<p class="text-sm font-medium text-neutral-200">Influence the Roadmap</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">Vote on features</p>
|
||||
</div>
|
||||
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center">
|
||||
<Clock class="w-6 h-6 text-oxide-500 mx-auto mb-3" />
|
||||
<p class="text-sm font-medium text-neutral-200">Lifetime Pricing Lock</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">Never pay more</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Email Capture -->
|
||||
<section id="join" class="py-16 border-t border-neutral-800">
|
||||
<div class="max-w-md mx-auto px-6">
|
||||
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">Get on the List</h2>
|
||||
<p class="text-neutral-400 text-center mb-8">Be first to know when early access opens.</p>
|
||||
|
||||
<div v-if="submitted" class="bg-green-500/10 border border-green-500/20 rounded-xl p-8 text-center">
|
||||
<Check class="w-10 h-10 text-green-400 mx-auto mb-3" />
|
||||
<h3 class="text-lg font-semibold text-neutral-100 mb-1">You're on the list.</h3>
|
||||
<p class="text-sm text-neutral-400">We'll reach out when early access opens.</p>
|
||||
</div>
|
||||
|
||||
<form v-else @submit.prevent="handleSubmit" class="space-y-4">
|
||||
<div v-if="errorMsg" class="p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm">
|
||||
{{ errorMsg }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="ea-email" class="block text-sm font-medium text-neutral-400 mb-1.5">Email</label>
|
||||
<input
|
||||
id="ea-email"
|
||||
v-model="email"
|
||||
type="email"
|
||||
required
|
||||
placeholder="admin@example.com"
|
||||
class="w-full px-3 py-2.5 bg-neutral-900 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ea-servers" class="block text-sm font-medium text-neutral-400 mb-1.5">How many servers do you run?</label>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<button
|
||||
v-for="option in ['1', '2-3', '4+']"
|
||||
:key="option"
|
||||
type="button"
|
||||
@click="serverCount = option"
|
||||
class="py-2.5 text-sm font-medium rounded-lg border transition-colors"
|
||||
:class="serverCount === option
|
||||
? 'bg-oxide-500/15 border-oxide-500/40 text-oxide-400'
|
||||
: 'bg-neutral-900 border-neutral-700 text-neutral-400 hover:border-neutral-600'"
|
||||
>
|
||||
{{ option }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="submitting || !email || !serverCount"
|
||||
class="w-full py-3 bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors"
|
||||
>
|
||||
{{ submitting ? 'Submitting...' : 'Join Early Access' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Founding Admin Program -->
|
||||
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800">
|
||||
<div class="max-w-3xl mx-auto px-6 text-center">
|
||||
<span class="inline-block px-3 py-1 bg-oxide-500/10 border border-oxide-500/20 rounded-full text-oxide-400 text-xs font-semibold uppercase tracking-wider mb-4">
|
||||
Limited to 25 Servers
|
||||
</span>
|
||||
<h2 class="text-3xl font-bold text-neutral-100 mb-4">Founding Admin Program</h2>
|
||||
<p class="text-neutral-400 mb-8">
|
||||
The first 25 servers to run Corrosion receive:
|
||||
</p>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
||||
<Star class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
||||
<p class="text-sm font-medium text-neutral-200">Founding Admin Role</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">Discord badge</p>
|
||||
</div>
|
||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
||||
<Clock class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
||||
<p class="text-sm font-medium text-neutral-200">Permanent Early Pricing</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">Locked forever</p>
|
||||
</div>
|
||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
||||
<Users class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
||||
<p class="text-sm font-medium text-neutral-200">Public Recognition</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">Featured server</p>
|
||||
</div>
|
||||
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
|
||||
<MessageCircle class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
|
||||
<p class="text-sm font-medium text-neutral-200">Direct Feature Input</p>
|
||||
<p class="text-xs text-neutral-500 mt-1">Private channel</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Demo Dashboard Preview -->
|
||||
<section id="demo" class="py-16 border-t border-neutral-800">
|
||||
<div class="max-w-5xl mx-auto px-6">
|
||||
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">Preview of Corrosion 1.0 Dashboard</h2>
|
||||
<p class="text-neutral-500 text-center mb-10">Screenshots will replace these frames at launch.</p>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div
|
||||
v-for="panel in panels"
|
||||
:key="panel.label"
|
||||
class="bg-neutral-900 border border-neutral-800 rounded-xl overflow-hidden hover:border-neutral-700 transition-colors"
|
||||
>
|
||||
<div class="h-36 bg-neutral-800/50 flex items-center justify-center border-b border-neutral-800">
|
||||
<component :is="panel.icon" class="w-10 h-10 text-neutral-700" />
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<h3 class="text-sm font-semibold text-neutral-200 mb-1">{{ panel.label }}</h3>
|
||||
<p class="text-xs text-neutral-500 leading-relaxed">{{ panel.desc }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Roadmap Voting -->
|
||||
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800">
|
||||
<div class="max-w-2xl mx-auto px-6">
|
||||
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">What Should We Build Next?</h2>
|
||||
<p class="text-neutral-500 text-center mb-8">Vote on the features that matter most to you.</p>
|
||||
<div class="space-y-3">
|
||||
<button
|
||||
v-for="item in voteItems"
|
||||
:key="item.id"
|
||||
@click="vote(item)"
|
||||
class="w-full flex items-center gap-4 p-4 bg-neutral-900 border rounded-xl transition-colors text-left"
|
||||
:class="item.voted ? 'border-oxide-500/30' : 'border-neutral-800 hover:border-neutral-700'"
|
||||
>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium" :class="item.voted ? 'text-oxide-400' : 'text-neutral-200'">{{ item.label }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<div class="w-24 h-1.5 bg-neutral-800 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="h-full rounded-full transition-all duration-300"
|
||||
:class="item.voted ? 'bg-oxide-500' : 'bg-neutral-600'"
|
||||
:style="{ width: `${totalVotes ? (item.votes / totalVotes) * 100 : 0}%` }"
|
||||
/>
|
||||
</div>
|
||||
<span class="text-xs font-medium tabular-nums w-8 text-right" :class="item.voted ? 'text-oxide-400' : 'text-neutral-500'">
|
||||
{{ item.votes }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Timeline -->
|
||||
<section class="py-16 border-t border-neutral-800">
|
||||
<div class="max-w-2xl mx-auto px-6">
|
||||
<h2 class="text-2xl font-bold text-neutral-100 mb-8 text-center">Launch Timeline</h2>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-8 h-8 bg-green-500/10 border border-green-500/20 rounded-full flex items-center justify-center shrink-0 mt-0.5">
|
||||
<Check class="w-4 h-4 text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-neutral-200">Week 1 — Closed Beta Stabilization</p>
|
||||
<p class="text-xs text-neutral-500 mt-0.5">Core platform hardening and testing.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 w-px h-4 bg-neutral-800" />
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-8 h-8 bg-oxide-500/10 border border-oxide-500/20 rounded-full flex items-center justify-center shrink-0 mt-0.5">
|
||||
<ChevronRight class="w-4 h-4 text-oxide-400" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-oxide-400">Week 2 — Early Access Opens</p>
|
||||
<p class="text-xs text-neutral-500 mt-0.5">Founding Admin licenses go live.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-4 w-px h-4 bg-neutral-800" />
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-8 h-8 bg-neutral-800 border border-neutral-700 rounded-full flex items-center justify-center shrink-0 mt-0.5">
|
||||
<ChevronRight class="w-4 h-4 text-neutral-500" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-neutral-400">Public Release</p>
|
||||
<p class="text-xs text-neutral-500 mt-0.5">Shortly after early access stabilization.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user