Session 8: Frontend Stripe cutover, soccer pages, sport selector, grade result cards, beta badge

This commit is contained in:
Kev
2026-06-10 15:34:23 -04:00
parent ad5ea8d5a8
commit 4db1c1c539
15 changed files with 1583 additions and 161 deletions
+168 -77
View File
@@ -1,6 +1,26 @@
'use client';
const TIERS = [
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
type TierId = 'free' | 'analyst' | 'desk';
interface TierConfig {
id: TierId;
name: string;
price: string;
originalPrice?: string;
cadence: string;
badge?: string;
headline: string;
cta: string;
features: string[];
locked: string[];
highlight: boolean;
}
const TIERS: TierConfig[] = [
{
id: 'free',
name: 'Free',
@@ -8,7 +28,6 @@ const TIERS = [
cadence: '/mo',
headline: 'Try the model. No card required.',
cta: 'Start Free',
ctaHref: '/signup',
features: [
'5 reads per month',
'Grade letter + projection',
@@ -31,7 +50,6 @@ const TIERS = [
badge: 'Founder Access',
headline: 'The full intelligence layer.',
cta: 'Lock Founder Price',
ctaHref: '/api/checkout?tier=analyst',
features: [
'Unlimited reads',
'Full factor analysis (40+ signals)',
@@ -54,7 +72,6 @@ const TIERS = [
cadence: '/mo',
headline: 'Everything. The professional setup.',
cta: 'Go Desk',
ctaHref: '/api/checkout?tier=desk',
features: [
'Everything in Analyst',
'Alt line ladder + edge ranking',
@@ -62,7 +79,6 @@ const TIERS = [
'Real-time intelligence feed',
'Parlay correlation analysis (phi)',
'Consensus vs model comparison',
'API access (coming Q3)',
],
locked: [],
highlight: false,
@@ -70,6 +86,51 @@ const TIERS = [
];
export default function Pricing() {
const router = useRouter();
const { session, loading: authLoading } = useAuth();
const [pending, setPending] = useState<TierId | null>(null);
const [error, setError] = useState<string | null>(null);
async function startCheckout(tier: TierId) {
setError(null);
// Free tier short-circuits — no checkout, just signup.
if (tier === 'free') {
router.push('/signup');
return;
}
// Anonymous → bounce to signup with a returnTo back to /#pricing.
if (!session) {
router.push('/signup?return=/%23pricing');
return;
}
setPending(tier);
try {
const res = await fetch('/api/checkout', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${session.access_token}`,
},
body: JSON.stringify({ tier }),
});
const data = (await res.json().catch(() => ({}))) as { url?: string; error?: string };
if (!res.ok || !data.url) {
setError(data.error || 'Checkout creation failed. Try again in a moment.');
setPending(null);
return;
}
// Hand off to Stripe. The success_url returns the user to
// /upgrade/success?session_id=… — no further client work needed.
window.location.assign(data.url);
} catch {
setError('Network error. Try again.');
setPending(null);
}
}
return (
<section
id="pricing"
@@ -87,89 +148,119 @@ export default function Pricing() {
Pricing built for bettors. Not for SaaS investors.
</h2>
<p style={{ fontSize: 17, color: 'var(--text-secondary)' }}>
First 100 users lock $14.99/mo for life. This price dies at user 101.
First 100 users lock $14.99/mo for life. Beta pricing this price dies at user 101.
</p>
</header>
{error && (
<div
role="alert"
style={{
maxWidth: 720,
margin: '0 auto 24px',
padding: 14,
border: '1px solid var(--grade-d, #ff5a5a)',
color: 'var(--grade-d, #ff5a5a)',
fontSize: 14,
textAlign: 'center',
borderRadius: 8,
}}
>
{error}
</div>
)}
<div className="pricing-grid" style={{ display: 'grid', gap: 24 }}>
{TIERS.map((tier, i) => (
<article
key={tier.id}
className={`surface diagonal-cut${tier.highlight ? ' diagonal-cut-strong' : ''} animate-fade-up stagger-${i + 1}`}
style={{
padding: 32,
position: 'relative',
border: tier.highlight ? '1px solid var(--grade-a)' : '1px solid var(--border)',
background: tier.highlight ? 'var(--bg-elevated)' : 'var(--bg-surface)',
boxShadow: tier.highlight ? '0 16px 48px var(--accent-glow)' : 'none',
}}
>
{tier.badge && (
<div
className="mono"
{TIERS.map((tier, i) => {
const isPending = pending === tier.id;
const isDisabled = authLoading || (pending !== null && !isPending);
return (
<article
key={tier.id}
className={`surface diagonal-cut${tier.highlight ? ' diagonal-cut-strong' : ''} animate-fade-up stagger-${i + 1}`}
style={{
padding: 32,
position: 'relative',
border: tier.highlight ? '1px solid var(--grade-a)' : '1px solid var(--border)',
background: tier.highlight ? 'var(--bg-elevated)' : 'var(--bg-surface)',
boxShadow: tier.highlight ? '0 16px 48px var(--accent-glow)' : 'none',
}}
>
{tier.badge && (
<div
className="mono"
style={{
position: 'absolute',
top: -12,
left: 24,
padding: '4px 12px',
background: 'var(--grade-a)',
color: 'var(--bg-primary)',
fontSize: 10,
fontWeight: 800,
letterSpacing: '0.08em',
borderRadius: 999,
textTransform: 'uppercase',
}}
>
{tier.badge}
</div>
)}
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
{tier.name}
</h3>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 4 }}>
<span className="mono" style={{ fontSize: 40, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-0.03em' }}>
{tier.price}
</span>
<span style={{ color: 'var(--text-tertiary)', fontSize: 14 }}>{tier.cadence}</span>
{tier.originalPrice && (
<span className="mono" style={{ fontSize: 13, color: 'var(--text-tertiary)', textDecoration: 'line-through' }}>
{tier.originalPrice}
</span>
)}
</div>
<p style={{ fontSize: 14, color: 'var(--text-secondary)', marginBottom: 24, minHeight: 42 }}>
{tier.headline}
</p>
<button
type="button"
onClick={() => startCheckout(tier.id)}
disabled={isDisabled}
className={tier.highlight ? 'btn-primary' : 'btn-ghost'}
style={{
position: 'absolute',
top: -12,
left: 24,
padding: '4px 12px',
background: 'var(--grade-a)',
color: 'var(--bg-primary)',
fontSize: 10,
fontWeight: 800,
letterSpacing: '0.08em',
borderRadius: 999,
textTransform: 'uppercase',
width: '100%',
padding: 14,
marginBottom: 24,
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.6 : 1,
}}
>
{tier.badge}
</div>
)}
<h3 style={{ fontSize: 14, fontWeight: 600, color: 'var(--text-secondary)', marginBottom: 4, textTransform: 'uppercase', letterSpacing: '0.08em' }}>
{tier.name}
</h3>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 4 }}>
<span className="mono" style={{ fontSize: 40, fontWeight: 800, color: 'var(--text-primary)', letterSpacing: '-0.03em' }}>
{tier.price}
</span>
<span style={{ color: 'var(--text-tertiary)', fontSize: 14 }}>{tier.cadence}</span>
{tier.originalPrice && (
<span className="mono" style={{ fontSize: 13, color: 'var(--text-tertiary)', textDecoration: 'line-through' }}>
{tier.originalPrice}
</span>
)}
</div>
<p style={{ fontSize: 14, color: 'var(--text-secondary)', marginBottom: 24, minHeight: 42 }}>
{tier.headline}
</p>
{isPending ? 'Redirecting to Stripe…' : tier.cta}
</button>
<a
href={tier.ctaHref}
className={tier.highlight ? 'btn-primary' : 'btn-ghost'}
style={{ width: '100%', padding: 14, marginBottom: 24 }}
>
{tier.cta}
</a>
<ul style={{ display: 'grid', gap: 10 }}>
{tier.features.map((f) => (
<li key={f} style={{ display: 'flex', gap: 10, fontSize: 14 }}>
<span style={{ color: 'var(--grade-a)', fontWeight: 700 }} aria-hidden>+</span>
<span style={{ color: 'var(--text-primary)' }}>{f}</span>
</li>
))}
{tier.locked.map((f) => (
<li key={f} style={{ display: 'flex', gap: 10, fontSize: 14, color: 'var(--text-tertiary)' }}>
<span aria-hidden></span>
<span>{f}</span>
</li>
))}
</ul>
</article>
))}
<ul style={{ display: 'grid', gap: 10 }}>
{tier.features.map((f) => (
<li key={f} style={{ display: 'flex', gap: 10, fontSize: 14 }}>
<span style={{ color: 'var(--grade-a)', fontWeight: 700 }} aria-hidden>+</span>
<span style={{ color: 'var(--text-primary)' }}>{f}</span>
</li>
))}
{tier.locked.map((f) => (
<li key={f} style={{ display: 'flex', gap: 10, fontSize: 14, color: 'var(--text-tertiary)' }}>
<span aria-hidden></span>
<span>{f}</span>
</li>
))}
</ul>
</article>
);
})}
</div>
<p style={{ textAlign: 'center', fontSize: 13, color: 'var(--text-tertiary)', marginTop: 32 }}>
Cancel anytime. No contracts. Card or Apple Pay or Google Pay payments processed by NexaPay.
Cancel anytime. No contracts. Card / Apple Pay / Google Pay payments processed by Stripe (test mode while we onboard founders).
</p>
</div>