Add pricing-page waitlist (name + email signup)

Replaces the disabled "Coming soon" buttons on the pricing page with a
waitlist band so visitors can be notified when alerts launch — separate
from registered users.

- waitlist_subscribers table (name, email unique, source, referrer)
- WaitlistService::subscribe — normalises email, idempotent
- Public POST /api/waitlist (throttle:10,1), thin controller + form request
- Read-only Filament resource with streamed CSV export
- Vue: useWaitlist composable + WaitlistForm, rendered below the grid
  while any tier is still "coming soon"; sends source + document.referrer

Announcement send mechanism deferred to a later task.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ovidiu U
2026-06-12 10:27:25 +01:00
parent e7d19488fd
commit 61adc133aa
15 changed files with 505 additions and 1 deletions

View File

@@ -80,13 +80,16 @@
<button v-else type="button" disabled class="w-full py-3 px-4 bg-zinc-100 rounded-xl text-center font-bold text-zinc-400 cursor-not-allowed">Coming soon</button>
</div>
</div>
<WaitlistForm v-if="hasComingSoon" source="pricing" />
</div>
</section>
</template>
<script setup>
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { useAuth } from '../composables/useAuth.js'
import WaitlistForm from './WaitlistForm.vue'
const { isAuthenticated, userTier } = useAuth()
@@ -102,6 +105,8 @@ const PRICE_SUFFIX = { monthly: '/mo', annual: '/yr' }
// disabled until then. Remove a tier from this list to make its button live.
const COMING_SOON = ['basic', 'plus']
const hasComingSoon = computed(() => COMING_SOON.length > 0)
function isComingSoon(tier) {
return COMING_SOON.includes(tier)
}

View File

@@ -0,0 +1,67 @@
<template>
<div class="max-w-2xl mx-auto mt-12">
<div class="bg-white border border-zinc-300 rounded-3xl p-8 text-center">
<template v-if="success">
<div class="flex flex-col items-center gap-3">
<span class="inline-flex items-center justify-center w-12 h-12 rounded-full bg-accent/10 text-accent">
<iconify-icon class="text-2xl" icon="lucide:check"></iconify-icon>
</span>
<h3 class="text-xl font-bold font-display text-zinc-800">You're on the list</h3>
<p class="text-zinc-500 text-sm">We'll email you the moment price alerts go live.</p>
</div>
</template>
<template v-else>
<h3 class="text-xl font-bold font-display text-zinc-800 mb-1">Want in when alerts launch?</h3>
<p class="text-zinc-500 text-sm mb-6">Leave your details and we'll let you know the day alerts go live.</p>
<form class="flex flex-col sm:flex-row gap-3" @submit.prevent="onSubmit">
<input
v-model.trim="name"
type="text"
autocomplete="name"
placeholder="Your name"
required
:disabled="loading"
class="flex-1 px-4 py-3 border border-zinc-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-60"
>
<input
v-model.trim="email"
type="email"
autocomplete="email"
placeholder="you@example.com"
required
:disabled="loading"
class="flex-1 px-4 py-3 border border-zinc-300 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-accent disabled:opacity-60"
>
<button
type="submit"
:disabled="loading"
class="px-6 py-3 bg-accent text-white rounded-xl font-bold text-sm shadow-lg hover:bg-primary-dark transition-all disabled:opacity-60 disabled:cursor-not-allowed whitespace-nowrap"
>
{{ loading ? 'Joining' : 'Notify me' }}
</button>
</form>
<p v-if="error" class="text-red-600 text-sm mt-3 text-left">{{ error }}</p>
</template>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useWaitlist } from '../composables/useWaitlist.js'
const props = defineProps({
source: { type: String, default: 'pricing' },
})
const name = ref('')
const email = ref('')
const { loading, success, error, submit } = useWaitlist()
function onSubmit() {
submit(name.value, email.value, props.source)
}
</script>