3f8bc58ca9
Phase 1 — Core API (Go): - Events, guests, tokens, RSVPs CRUD on PostgreSQL via pgx/v5 - HMAC-signed per-guest tokens with format validation - Health endpoint with DB ping, slog JSON logging, graceful shutdown Phase 2 — NATS + Fraud Engine: - NATS JetStream pub/sub with explicit-ack consumers - Python/FastAPI fraud engine with heuristic risk scoring (fingerprint mismatch, IP change, missing signals, repeated access) - gRPC sync scoring with 250ms fail-open timeout - Per-guest baseline tracking; risk bands low/medium/high/block Phase 3 — Notifications + Frontend: - Notification worker scaffolding (Twilio/SES stubs, retry/backoff) - Nuxt 3 frontend with Tailwind dark theme + brand green - Live monitor via WebSocket with auto-reconnect - Activity history endpoint backfills monitor with RSVPs + scored access checks (including blocked attempts) UX polish: - Marketing-friendly landing page (hero mockup, how-it-works, features, use cases, testimonials, FAQ, final CTA) - Animated layered card mockups on landing + new-event page - Plus-ones stepper, RSVP status badges, filter buttons - Friendly access-check labels (Verified/Review/Suspicious/Blocked) - Dashboard hydration fix via ClientOnly wrapper Infrastructure: - docker-compose for full local dev (postgres, nats, api, fraud-engine, notifier, frontend) - Multi-stage Dockerfiles, non-root UID 1000 - Integration tests with testcontainers-go Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
555 lines
31 KiB
Vue
555 lines
31 KiB
Vue
<script setup lang="ts">
|
|
useSeoMeta({
|
|
title: 'GuestGuard — Stress-free RSVPs for every occasion',
|
|
description: 'Send personal invitations, track RSVPs in real time, and keep your guest list exactly as you planned it — weddings, parties, corporate events and more.',
|
|
})
|
|
|
|
// =============================================================
|
|
// Hero mockup: animated stat + cycling activity pill
|
|
// =============================================================
|
|
interface HeroActivity {
|
|
id: number
|
|
name: string
|
|
initials: string
|
|
action: string
|
|
extra?: string | null
|
|
tone: 'attending' | 'declined' | 'maybe'
|
|
}
|
|
|
|
const heroPool: Omit<HeroActivity, 'id'>[] = [
|
|
{ name: 'Aisha B.', initials: 'AB', action: 'just confirmed', extra: '+2 guests', tone: 'attending' },
|
|
{ name: 'Marcus Chen', initials: 'MC', action: 'is attending', extra: null, tone: 'attending' },
|
|
{ name: 'Sofia Rivera', initials: 'SR', action: 'replied maybe', extra: '+1 guest', tone: 'maybe' },
|
|
{ name: 'John Doe', initials: 'JD', action: 'just confirmed', extra: '+3 guests', tone: 'attending' },
|
|
{ name: 'Priya Sharma', initials: 'PS', action: 'is attending', extra: '+1 guest', tone: 'attending' },
|
|
]
|
|
|
|
let heroCounter = 0
|
|
const heroActivity = ref<HeroActivity>({ ...heroPool[0], id: ++heroCounter })
|
|
const liveConfirmed = ref(42)
|
|
|
|
let heroActTimer: ReturnType<typeof setInterval> | null = null
|
|
let heroCountTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
onMounted(() => {
|
|
if (!import.meta.client) return
|
|
|
|
let idx = 1
|
|
heroActTimer = setInterval(() => {
|
|
heroActivity.value = { ...heroPool[idx % heroPool.length], id: ++heroCounter }
|
|
idx++
|
|
}, 3000)
|
|
|
|
// Slow ticking count-up — 42 → 47, then loops
|
|
heroCountTimer = setInterval(() => {
|
|
liveConfirmed.value = liveConfirmed.value >= 47 ? 42 : liveConfirmed.value + 1
|
|
}, 4200)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (heroActTimer) clearInterval(heroActTimer)
|
|
if (heroCountTimer) clearInterval(heroCountTimer)
|
|
})
|
|
|
|
const heroToneClass: Record<HeroActivity['tone'], string> = {
|
|
attending: 'bg-brand-500/20 text-brand-300',
|
|
declined: 'bg-zinc-700/50 text-zinc-300',
|
|
maybe: 'bg-amber-500/20 text-amber-300',
|
|
}
|
|
|
|
// =============================================================
|
|
// FAQ items
|
|
// =============================================================
|
|
const faqs = [
|
|
{
|
|
q: 'Do my guests need to create an account to RSVP?',
|
|
a: 'No. They simply click the personal link you send them and respond — no sign-up, no password, no friction. It works on any phone or computer.',
|
|
},
|
|
{
|
|
q: 'Is GuestGuard free to start?',
|
|
a: 'Yes. You can plan your first event, send invitations, and collect RSVPs without paying a cent. No credit card required to get going.',
|
|
},
|
|
{
|
|
q: 'What happens if someone forwards their invitation to a friend?',
|
|
a: "We notice. Each link is tied to the original guest, and our system quietly checks for unusual behaviour — like the link being used from a different device or location. You'll see the alert on your dashboard.",
|
|
},
|
|
{
|
|
q: 'Can I import my guest list from a spreadsheet?',
|
|
a: 'Absolutely. Drop in your CSV with names, emails, and phone numbers, and GuestGuard will handle the rest.',
|
|
},
|
|
{
|
|
q: 'Will guests notice all the security checks happening behind the scenes?',
|
|
a: 'Not at all. From their side it feels like a simple, beautiful RSVP page. The protection happens invisibly so the experience stays delightful.',
|
|
},
|
|
]
|
|
</script>
|
|
|
|
<template>
|
|
<section>
|
|
<!-- ============================================================ -->
|
|
<!-- 1. HERO -->
|
|
<!-- ============================================================ -->
|
|
<div class="grid gap-12 py-12 md:py-16 lg:grid-cols-[1.1fr_1fr] lg:items-center lg:gap-16 lg:py-20">
|
|
<div>
|
|
<p class="mb-5 inline-flex items-center gap-2 rounded-full border border-brand-900/60 bg-brand-950/40 px-3 py-1 text-xs font-medium text-brand-400">
|
|
<span class="h-1.5 w-1.5 rounded-full bg-brand-400"></span>
|
|
For Weddings, Parties & Every Gathering in Between
|
|
</p>
|
|
<h1 class="mb-6 text-4xl font-semibold leading-tight tracking-tight text-zinc-50 md:text-5xl">
|
|
Know Exactly Who's Coming<br />
|
|
<span class="text-brand-500">Before the Big Day.</span>
|
|
</h1>
|
|
<p class="mb-8 max-w-xl text-lg leading-relaxed text-zinc-400">
|
|
Send each guest a personal invitation link, watch RSVPs roll in on your live dashboard,
|
|
and let GuestGuard quietly handle the rest — so you can focus on making your
|
|
event unforgettable.
|
|
</p>
|
|
|
|
<div class="mb-6 flex flex-wrap gap-3">
|
|
<NuxtLink to="/dashboard" class="btn-primary px-6 py-2.5 text-base">
|
|
Start Planning →
|
|
</NuxtLink>
|
|
<a href="#how-it-works" class="btn-ghost px-6 py-2.5 text-base">
|
|
See How It Works
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Trust strip -->
|
|
<div class="flex flex-wrap items-center gap-x-5 gap-y-2 text-xs text-zinc-500">
|
|
<span class="flex items-center gap-1.5">
|
|
<svg class="h-3.5 w-3.5 text-brand-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Free to start
|
|
</span>
|
|
<span class="flex items-center gap-1.5">
|
|
<svg class="h-3.5 w-3.5 text-brand-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
No credit card needed
|
|
</span>
|
|
<span class="flex items-center gap-1.5">
|
|
<svg class="h-3.5 w-3.5 text-brand-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
Set up in 2 minutes
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- =================== HERO VISUAL MOCKUP =================== -->
|
|
<div class="relative mx-auto w-full max-w-sm py-6 lg:max-w-md">
|
|
<!-- Soft brand glow backdrop -->
|
|
<div class="pointer-events-none absolute -inset-10 bg-gradient-to-br from-brand-500/25 via-transparent to-brand-500/10 blur-3xl"></div>
|
|
|
|
<!-- Floating sparkle dots -->
|
|
<div class="pointer-events-none absolute -left-4 top-8 h-2 w-2 rounded-full bg-brand-400 opacity-70" style="animation: gg-ping 2.6s cubic-bezier(0,0,.2,1) infinite"></div>
|
|
<div class="pointer-events-none absolute right-6 -top-2 h-1.5 w-1.5 rounded-full bg-brand-300 opacity-70" style="animation: gg-ping 3.2s cubic-bezier(0,0,.2,1) infinite; animation-delay: .6s"></div>
|
|
<div class="pointer-events-none absolute -right-2 bottom-20 h-2 w-2 rounded-full bg-brand-500 opacity-60" style="animation: gg-ping 3.6s cubic-bezier(0,0,.2,1) infinite; animation-delay: 1.2s"></div>
|
|
<div class="pointer-events-none absolute left-8 -bottom-2 h-1.5 w-1.5 rounded-full bg-brand-400 opacity-60" style="animation: gg-ping 2.8s cubic-bezier(0,0,.2,1) infinite; animation-delay: 1.8s"></div>
|
|
|
|
<!-- 1. Stats card (top-right, floating, tilts +3°) -->
|
|
<div
|
|
class="absolute -right-2 -top-6 z-20 rounded-xl border border-zinc-800 bg-zinc-900/95 px-4 py-3 shadow-2xl backdrop-blur md:right-0"
|
|
style="animation: gg-float-cw 5.5s ease-in-out infinite"
|
|
>
|
|
<p class="mb-1.5 flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wider text-brand-400">
|
|
<span class="relative flex h-1.5 w-1.5">
|
|
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-brand-400 opacity-75"></span>
|
|
<span class="relative inline-flex h-1.5 w-1.5 rounded-full bg-brand-400"></span>
|
|
</span>
|
|
Live RSVPs
|
|
</p>
|
|
<div class="flex items-baseline gap-2 text-zinc-100">
|
|
<span class="text-2xl font-bold tabular-nums transition-all duration-300">{{ liveConfirmed }}</span>
|
|
<span class="text-xs text-zinc-500">of 60 confirmed</span>
|
|
</div>
|
|
<!-- Progress bar -->
|
|
<div class="mt-2 h-1 w-full overflow-hidden rounded-full bg-zinc-800">
|
|
<div
|
|
class="h-full rounded-full bg-gradient-to-r from-brand-500 to-brand-400 transition-all duration-700 ease-out"
|
|
:style="{ width: `${(liveConfirmed / 60) * 100}%` }"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 2. Main invitation card (centre, floating, tilts -2°) -->
|
|
<div
|
|
class="relative z-10 overflow-hidden rounded-2xl border border-zinc-800 bg-gradient-to-br from-zinc-900 via-zinc-900 to-zinc-950 shadow-2xl"
|
|
style="animation: gg-float-ccw 6s ease-in-out infinite"
|
|
>
|
|
<div class="h-1 bg-gradient-to-r from-brand-600 via-brand-400 to-brand-600"></div>
|
|
<div class="p-6">
|
|
<p class="mb-2 text-[10px] font-medium uppercase tracking-[0.22em] text-brand-400">
|
|
✦ You're Invited
|
|
</p>
|
|
<h3 class="mb-1 text-xl font-semibold text-zinc-100">Sarah & James</h3>
|
|
<p class="mb-4 text-xs text-zinc-500">The Grand Ballroom · Sat, Jun 15</p>
|
|
|
|
<p class="mb-2 text-xs text-zinc-400">Will you be there?</p>
|
|
<div class="flex gap-1.5">
|
|
<span class="flex-1 rounded-md border border-brand-700/60 bg-brand-950/40 px-2 py-1.5 text-center text-xs font-medium text-brand-300">Attending</span>
|
|
<span class="flex-1 rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-center text-xs text-zinc-500">Maybe</span>
|
|
<span class="flex-1 rounded-md border border-zinc-700 bg-zinc-900 px-2 py-1.5 text-center text-xs text-zinc-500">Decline</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 3. Activity pill (bottom-left, floating, tilts -3°) -->
|
|
<div
|
|
class="absolute -bottom-6 -left-2 z-20 min-w-[14rem] rounded-xl border border-zinc-800 bg-zinc-900/95 px-4 py-2.5 shadow-2xl backdrop-blur md:left-2"
|
|
style="animation: gg-float-ccw-sm 5s ease-in-out infinite; animation-delay: .4s"
|
|
>
|
|
<Transition
|
|
enter-active-class="transition duration-500 ease-out"
|
|
enter-from-class="-translate-y-2 opacity-0"
|
|
enter-to-class="translate-y-0 opacity-100"
|
|
leave-active-class="transition duration-300 ease-in absolute inset-x-4 inset-y-2.5"
|
|
leave-from-class="translate-y-0 opacity-100"
|
|
leave-to-class="translate-y-2 opacity-0"
|
|
mode="out-in"
|
|
>
|
|
<div :key="heroActivity.id" class="flex items-center gap-2.5">
|
|
<span
|
|
class="flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold"
|
|
:class="heroToneClass[heroActivity.tone]"
|
|
>
|
|
{{ heroActivity.initials }}
|
|
</span>
|
|
<div class="min-w-0 text-xs">
|
|
<p class="truncate font-medium text-zinc-100">
|
|
{{ heroActivity.name }}
|
|
<span class="font-normal text-zinc-500">{{ heroActivity.action }}</span>
|
|
</p>
|
|
<p class="text-[10px] text-zinc-500">
|
|
<span v-if="heroActivity.extra" class="text-brand-400">{{ heroActivity.extra }} · </span>
|
|
a moment ago
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
|
|
<!-- 4. Security toast (bottom-right, floating, tilts +2°) -->
|
|
<div
|
|
class="absolute -bottom-2 -right-3 z-20 rounded-xl border border-amber-900/40 bg-zinc-900/95 px-3.5 py-2 shadow-2xl backdrop-blur md:right-0"
|
|
style="animation: gg-float-cw-sm 5.8s ease-in-out infinite; animation-delay: 1s"
|
|
>
|
|
<div class="flex items-center gap-2">
|
|
<span class="flex h-6 w-6 items-center justify-center rounded-full bg-amber-500/20 text-amber-300">
|
|
<svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
</svg>
|
|
</span>
|
|
<div class="text-xs">
|
|
<p class="font-medium text-zinc-100">Forwarded link blocked</p>
|
|
<p class="text-[10px] text-zinc-500">Suspicious activity flagged</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- 2. HOW IT WORKS -->
|
|
<!-- ============================================================ -->
|
|
<div id="how-it-works" class="border-t border-zinc-900 py-16 md:py-20">
|
|
<div class="mb-12 text-center">
|
|
<p class="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-brand-500">How It Works</p>
|
|
<h2 class="mb-3 text-3xl font-semibold tracking-tight text-zinc-50 md:text-4xl">
|
|
From Guest List to Confirmed Seats,<br />in Three Simple Steps.
|
|
</h2>
|
|
</div>
|
|
|
|
<div class="relative grid gap-6 md:grid-cols-3">
|
|
<!-- Connector line (hidden on mobile) -->
|
|
<div class="pointer-events-none absolute left-[16%] right-[16%] top-7 hidden h-px bg-gradient-to-r from-transparent via-zinc-800 to-transparent md:block"></div>
|
|
|
|
<div class="relative text-center">
|
|
<div class="relative z-10 mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl border border-brand-900/60 bg-gradient-to-br from-brand-950 to-zinc-900 text-lg font-bold text-brand-400 shadow-lg">
|
|
1
|
|
</div>
|
|
<h3 class="mb-2 font-semibold text-zinc-100">Add Your Guest List</h3>
|
|
<p class="mx-auto max-w-xs text-sm leading-relaxed text-zinc-400">
|
|
Type names in, paste from a spreadsheet, or upload a CSV. We'll handle the rest.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="relative text-center">
|
|
<div class="relative z-10 mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl border border-brand-900/60 bg-gradient-to-br from-brand-950 to-zinc-900 text-lg font-bold text-brand-400 shadow-lg">
|
|
2
|
|
</div>
|
|
<h3 class="mb-2 font-semibold text-zinc-100">Send Personal Links</h3>
|
|
<p class="mx-auto max-w-xs text-sm leading-relaxed text-zinc-400">
|
|
Each guest gets their own private invitation — share it by WhatsApp, email,
|
|
or however you like.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="relative text-center">
|
|
<div class="relative z-10 mx-auto mb-5 flex h-14 w-14 items-center justify-center rounded-2xl border border-brand-900/60 bg-gradient-to-br from-brand-950 to-zinc-900 text-lg font-bold text-brand-400 shadow-lg">
|
|
3
|
|
</div>
|
|
<h3 class="mb-2 font-semibold text-zinc-100">Watch RSVPs Roll In</h3>
|
|
<p class="mx-auto max-w-xs text-sm leading-relaxed text-zinc-400">
|
|
Confirmations appear on your dashboard as they happen. Final headcount,
|
|
ready when you are.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- 3. FEATURES -->
|
|
<!-- ============================================================ -->
|
|
<div class="border-t border-zinc-900 py-16 md:py-20">
|
|
<div class="mb-12 text-center">
|
|
<p class="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-brand-500">Why GuestGuard</p>
|
|
<h2 class="text-3xl font-semibold tracking-tight text-zinc-50 md:text-4xl">
|
|
Everything You Need, Nothing You Don't.
|
|
</h2>
|
|
</div>
|
|
|
|
<div class="grid gap-6 md:grid-cols-3">
|
|
<div class="card group transition hover:border-brand-800/60">
|
|
<div class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand-500/10 transition group-hover:bg-brand-500/20">
|
|
<svg class="h-5 w-5 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="mb-2 font-semibold text-zinc-100">Personal Invitations</h3>
|
|
<p class="text-sm leading-relaxed text-zinc-400">
|
|
Every guest gets their own private link — no public sign-up forms, no
|
|
gate-crashing, and no way for a forwarded link to let the wrong person in.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card group transition hover:border-brand-800/60">
|
|
<div class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand-500/10 transition group-hover:bg-brand-500/20">
|
|
<svg class="h-5 w-5 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="mb-2 font-semibold text-zinc-100">Live RSVP Dashboard</h3>
|
|
<p class="text-sm leading-relaxed text-zinc-400">
|
|
Watch confirmations roll in the moment guests respond. See who's attending,
|
|
who's declined, and who still hasn't replied — all filtered in one clean view.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card group transition hover:border-brand-800/60">
|
|
<div class="mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand-500/10 transition group-hover:bg-brand-500/20">
|
|
<svg class="h-5 w-5 text-brand-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
|
</svg>
|
|
</div>
|
|
<h3 class="mb-2 font-semibold text-zinc-100">Built-in Protection</h3>
|
|
<p class="text-sm leading-relaxed text-zinc-400">
|
|
Our system quietly watches for anything unusual — like the same link being
|
|
used from two different places — and flags it before it becomes your problem.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- 4. PERFECT FOR -->
|
|
<!-- ============================================================ -->
|
|
<div class="border-t border-zinc-900 py-16 md:py-20">
|
|
<div class="mb-12 text-center">
|
|
<p class="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-brand-500">Perfect For</p>
|
|
<h2 class="mb-3 text-3xl font-semibold tracking-tight text-zinc-50 md:text-4xl">
|
|
Any Gathering Where Your Guest List Matters.
|
|
</h2>
|
|
<p class="mx-auto max-w-2xl text-zinc-500">
|
|
Whether you're hosting six or six hundred, GuestGuard fits the way you plan.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-6">
|
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 text-center transition hover:border-brand-800/60 hover:bg-zinc-900">
|
|
<div class="mx-auto mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-brand-500/10 text-brand-400">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-zinc-100">Weddings</p>
|
|
</div>
|
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 text-center transition hover:border-brand-800/60 hover:bg-zinc-900">
|
|
<div class="mx-auto mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-brand-500/10 text-brand-400">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8a3 3 0 100-6 3 3 0 000 6zm0 0v4m-7 8h14M5 16a7 7 0 0114 0v4H5v-4z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-zinc-100">Birthdays</p>
|
|
</div>
|
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 text-center transition hover:border-brand-800/60 hover:bg-zinc-900">
|
|
<div class="mx-auto mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-brand-500/10 text-brand-400">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0H5m14 0h2m-2 0v-3m-4 3v-7H9v7m4 0h-4m4 0h4M9 7h1m-1 4h1m4-4h1m-1 4h1" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-zinc-100">Corporate</p>
|
|
</div>
|
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 text-center transition hover:border-brand-800/60 hover:bg-zinc-900">
|
|
<div class="mx-auto mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-brand-500/10 text-brand-400">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8v13m0-13V6a2 2 0 112 2h-2zm0 0V5.5A2.5 2.5 0 109.5 8H12zm-7 4h14M5 12a2 2 0 110-4h14a2 2 0 110 4M5 12v7a2 2 0 002 2h10a2 2 0 002-2v-7" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-zinc-100">Anniversaries</p>
|
|
</div>
|
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 text-center transition hover:border-brand-800/60 hover:bg-zinc-900">
|
|
<div class="mx-auto mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-brand-500/10 text-brand-400">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.631 8.41m5.96 5.96a14.926 14.926 0 01-5.841 2.58m-.119-8.54a6 6 0 00-7.381 5.84h4.8m2.581-5.84a14.927 14.927 0 00-2.58 5.84m2.699 2.7c-.103.021-.207.041-.311.06a15.09 15.09 0 01-2.448-2.448 14.9 14.9 0 01.06-.312m-2.24 2.39a4.493 4.493 0 00-1.757 4.306 4.493 4.493 0 004.306-1.758M16.5 9a1.5 1.5 0 11-3 0 1.5 1.5 0 013 0z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-zinc-100">Launches</p>
|
|
</div>
|
|
<div class="rounded-xl border border-zinc-800 bg-zinc-900/40 p-4 text-center transition hover:border-brand-800/60 hover:bg-zinc-900">
|
|
<div class="mx-auto mb-2 flex h-9 w-9 items-center justify-center rounded-lg bg-brand-500/10 text-brand-400">
|
|
<svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.6">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
</svg>
|
|
</div>
|
|
<p class="text-sm font-medium text-zinc-100">Private Dinners</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- 5. TESTIMONIALS -->
|
|
<!-- ============================================================ -->
|
|
<div class="border-t border-zinc-900 py-16 md:py-20">
|
|
<div class="mb-12 text-center">
|
|
<p class="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-brand-500">Loved by Hosts</p>
|
|
<h2 class="text-3xl font-semibold tracking-tight text-zinc-50 md:text-4xl">
|
|
A Little Peace of Mind Goes a Long Way.
|
|
</h2>
|
|
</div>
|
|
|
|
<div class="grid gap-6 md:grid-cols-3">
|
|
<figure class="card">
|
|
<div class="mb-4 flex gap-0.5 text-brand-400">
|
|
<svg v-for="n in 5" :key="n" class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.957a1 1 0 00.95.69h4.162c.969 0 1.371 1.24.588 1.81l-3.367 2.446a1 1 0 00-.364 1.118l1.287 3.957c.3.922-.755 1.688-1.54 1.118L10 13.347l-3.367 2.446c-.784.57-1.838-.196-1.539-1.118l1.286-3.957a1 1 0 00-.364-1.118L2.65 9.154c-.784-.57-.38-1.81.588-1.81h4.162a1 1 0 00.95-.69l1.286-3.957z" />
|
|
</svg>
|
|
</div>
|
|
<blockquote class="mb-5 text-sm leading-relaxed text-zinc-300">
|
|
"Planning my daughter's wedding was already overwhelming — GuestGuard made the RSVP
|
|
part the easiest piece. I knew exactly who was coming and didn't have to chase a single soul."
|
|
</blockquote>
|
|
<figcaption class="flex items-center gap-3">
|
|
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-brand-500/20 text-sm font-semibold text-brand-300">EC</span>
|
|
<div>
|
|
<p class="text-sm font-medium text-zinc-100">Emma Carter</p>
|
|
<p class="text-xs text-zinc-500">Mother of the bride · London</p>
|
|
</div>
|
|
</figcaption>
|
|
</figure>
|
|
|
|
<figure class="card">
|
|
<div class="mb-4 flex gap-0.5 text-brand-400">
|
|
<svg v-for="n in 5" :key="n" class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.957a1 1 0 00.95.69h4.162c.969 0 1.371 1.24.588 1.81l-3.367 2.446a1 1 0 00-.364 1.118l1.287 3.957c.3.922-.755 1.688-1.54 1.118L10 13.347l-3.367 2.446c-.784.57-1.838-.196-1.539-1.118l1.286-3.957a1 1 0 00-.364-1.118L2.65 9.154c-.784-.57-.38-1.81.588-1.81h4.162a1 1 0 00.95-.69l1.286-3.957z" />
|
|
</svg>
|
|
</div>
|
|
<blockquote class="mb-5 text-sm leading-relaxed text-zinc-300">
|
|
"We host quarterly partner events. Cutting no-shows by sending personal links
|
|
instead of a public form has been huge for our catering budget."
|
|
</blockquote>
|
|
<figcaption class="flex items-center gap-3">
|
|
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-brand-500/20 text-sm font-semibold text-brand-300">MC</span>
|
|
<div>
|
|
<p class="text-sm font-medium text-zinc-100">Marcus Chen</p>
|
|
<p class="text-xs text-zinc-500">Corporate events lead · Singapore</p>
|
|
</div>
|
|
</figcaption>
|
|
</figure>
|
|
|
|
<figure class="card">
|
|
<div class="mb-4 flex gap-0.5 text-brand-400">
|
|
<svg v-for="n in 5" :key="n" class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.286 3.957a1 1 0 00.95.69h4.162c.969 0 1.371 1.24.588 1.81l-3.367 2.446a1 1 0 00-.364 1.118l1.287 3.957c.3.922-.755 1.688-1.54 1.118L10 13.347l-3.367 2.446c-.784.57-1.838-.196-1.539-1.118l1.286-3.957a1 1 0 00-.364-1.118L2.65 9.154c-.784-.57-.38-1.81.588-1.81h4.162a1 1 0 00.95-.69l1.286-3.957z" />
|
|
</svg>
|
|
</div>
|
|
<blockquote class="mb-5 text-sm leading-relaxed text-zinc-300">
|
|
"My birthday party went from a stressful headcount nightmare to something I was
|
|
actually looking forward to. Everyone got a beautiful link, and that was it."
|
|
</blockquote>
|
|
<figcaption class="flex items-center gap-3">
|
|
<span class="flex h-9 w-9 items-center justify-center rounded-full bg-brand-500/20 text-sm font-semibold text-brand-300">PS</span>
|
|
<div>
|
|
<p class="text-sm font-medium text-zinc-100">Priya Sharma</p>
|
|
<p class="text-xs text-zinc-500">Birthday host · Toronto</p>
|
|
</div>
|
|
</figcaption>
|
|
</figure>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- 6. FAQ -->
|
|
<!-- ============================================================ -->
|
|
<div class="border-t border-zinc-900 py-16 md:py-20">
|
|
<div class="mb-12 text-center">
|
|
<p class="mb-3 text-xs font-medium uppercase tracking-[0.2em] text-brand-500">Questions, Answered</p>
|
|
<h2 class="text-3xl font-semibold tracking-tight text-zinc-50 md:text-4xl">
|
|
The Things Hosts Usually Ask Us.
|
|
</h2>
|
|
</div>
|
|
|
|
<div class="mx-auto max-w-3xl space-y-3">
|
|
<details
|
|
v-for="(item, i) in faqs"
|
|
:key="i"
|
|
class="group rounded-xl border border-zinc-800 bg-zinc-900/40 px-5 py-4 transition open:border-brand-900/40 open:bg-zinc-900"
|
|
>
|
|
<summary class="flex cursor-pointer list-none items-center justify-between gap-4">
|
|
<span class="font-medium text-zinc-100">{{ item.q }}</span>
|
|
<svg
|
|
class="h-5 w-5 shrink-0 text-zinc-500 transition group-open:rotate-45 group-open:text-brand-400"
|
|
fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"
|
|
>
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
|
|
</svg>
|
|
</summary>
|
|
<p class="mt-3 text-sm leading-relaxed text-zinc-400">{{ item.a }}</p>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- 7. FINAL CTA -->
|
|
<!-- ============================================================ -->
|
|
<div class="relative my-16 overflow-hidden rounded-3xl border border-brand-900/40 bg-gradient-to-br from-brand-950/40 via-zinc-900 to-zinc-950 p-10 text-center md:p-16">
|
|
<div class="pointer-events-none absolute -left-20 -top-20 h-72 w-72 rounded-full bg-brand-500/10 blur-3xl"></div>
|
|
<div class="pointer-events-none absolute -bottom-20 -right-20 h-72 w-72 rounded-full bg-brand-500/10 blur-3xl"></div>
|
|
|
|
<div class="relative">
|
|
<h2 class="mb-4 text-3xl font-semibold tracking-tight text-zinc-50 md:text-4xl">
|
|
Ready to Host With Confidence?
|
|
</h2>
|
|
<p class="mx-auto mb-8 max-w-xl text-zinc-400">
|
|
Your next event deserves a perfect guest list. Get started in two minutes —
|
|
no credit card, no commitment.
|
|
</p>
|
|
<div class="flex flex-wrap justify-center gap-3">
|
|
<NuxtLink to="/dashboard" class="btn-primary px-6 py-3 text-base">
|
|
Start Planning Your Event →
|
|
</NuxtLink>
|
|
</div>
|
|
<p class="mt-6 text-xs text-zinc-500">
|
|
Already have an account?
|
|
<NuxtLink to="/dashboard" class="text-brand-400 hover:text-brand-300">Sign in here</NuxtLink>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
|
|
<style scoped>
|
|
summary::-webkit-details-marker { display: none; }
|
|
</style>
|