feat: Feature 3.1 — Landing page + blog + Phase 3 specs

Next.js 14+ web app in web/ directory:
- Landing page: Hero, How It Works, Features, 3-tier Pricing with
  founder badges, Footer with email capture
- Blog system: MDX-powered, /blog index + /blog/[slug] pages,
  reading time, Open Graph tags, JSON-LD structured data
- Auth pages: /login + /signup (Supabase Auth ready)
- Design system: dark theme, grade colors (A/B/C/D), BetonBLK voice
- 1 seed blog post: "How to Read Line Movement Like a Sharp"
- Specs for 3.2 (Scan UI), 3.3 (Bet Tracker), 3.4 (Stripe)

Build passes clean: 7 static pages generated.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-03-22 09:43:38 -04:00
parent ed6502a880
commit bfa8345ebf
26 changed files with 5142 additions and 31 deletions
+71
View File
@@ -0,0 +1,71 @@
import { getAllPosts, getPostBySlug } from '@/lib/blog';
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
export async function generateStaticParams() {
const posts = getAllPosts();
return posts.map((post) => ({ slug: post.slug }));
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) return {};
return {
title: `${post.title} — BetonBLK Blog`,
description: post.description,
openGraph: {
title: post.title,
description: post.description,
type: 'article',
publishedTime: post.date,
},
};
}
export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const post = getPostBySlug(slug);
if (!post) notFound();
return (
<article className="py-16 px-4">
<div className="max-w-3xl mx-auto">
<a href="/blog" className="text-sm text-[var(--text-muted)] hover:text-white transition mb-8 inline-block">
&larr; Back to Blog
</a>
<div className="flex items-center gap-3 text-xs text-[var(--text-muted)] mb-4">
<time>{post.date}</time>
<span>{post.readingTime} min read</span>
</div>
<h1 className="text-4xl font-bold mb-6">{post.title}</h1>
{post.tags.length > 0 && (
<div className="flex gap-2 mb-8">
{post.tags.map((tag) => (
<span key={tag} className="px-2 py-0.5 text-xs rounded-full bg-[var(--border)] text-[var(--text-muted)]">
{tag}
</span>
))}
</div>
)}
<div className="prose prose-invert max-w-none text-[var(--text)] leading-relaxed whitespace-pre-wrap">
{post.content}
</div>
</div>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.description,
datePublished: post.date,
author: { '@type': 'Organization', name: 'BetonBLK' },
}),
}}
/>
</article>
);
}
+50
View File
@@ -0,0 +1,50 @@
import { getAllPosts } from '@/lib/blog';
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Blog — BetonBLK',
description: 'Betting strategy, prop analysis breakdowns, and product updates from BetonBLK.',
};
export default function BlogIndex() {
const posts = getAllPosts();
return (
<section className="py-16 px-4">
<div className="max-w-3xl mx-auto">
<h1 className="text-4xl font-bold mb-2">Blog</h1>
<p className="text-[var(--text-muted)] mb-12">Strategy. Analysis. Updates.</p>
{posts.length === 0 ? (
<p className="text-[var(--text-muted)]">Posts coming soon.</p>
) : (
<div className="space-y-8">
{posts.map((post) => (
<a
key={post.slug}
href={`/blog/${post.slug}`}
className="block p-6 rounded-xl bg-[var(--card)] border border-[var(--border)] hover:border-[var(--accent)] transition"
>
<div className="flex items-center gap-3 text-xs text-[var(--text-muted)] mb-2">
<time>{post.date}</time>
<span>{post.readingTime} min read</span>
</div>
<h2 className="text-xl font-semibold mb-1">{post.title}</h2>
<p className="text-sm text-[var(--text-muted)]">{post.description}</p>
{post.tags.length > 0 && (
<div className="flex gap-2 mt-3">
{post.tags.map((tag) => (
<span key={tag} className="px-2 py-0.5 text-xs rounded-full bg-[var(--border)] text-[var(--text-muted)]">
{tag}
</span>
))}
</div>
)}
</a>
))}
</div>
)}
</div>
</section>
);
}
+24
View File
@@ -0,0 +1,24 @@
@import "tailwindcss";
:root {
--bg: #0a0a0a;
--card: #141414;
--border: #222222;
--text: #e0e0e0;
--text-muted: #888888;
--grade-a: #22c55e;
--grade-b: #eab308;
--grade-c: #f97316;
--grade-d: #ef4444;
--accent: #3b82f6;
}
body {
background: var(--bg);
color: var(--text);
font-family: 'Inter', system-ui, sans-serif;
}
.font-mono {
font-family: 'JetBrains Mono', monospace;
}
+45
View File
@@ -0,0 +1,45 @@
import type { Metadata } from 'next';
import './globals.css';
export const metadata: Metadata = {
title: 'BetonBLK — AI-Powered Parlay Intelligence',
description: 'Stop guessing. Start grading. BetonBLK scans your parlay in seconds with AI-powered prop analysis across DraftKings, FanDuel, and BetMGM.',
openGraph: {
title: 'BetonBLK — AI-Powered Parlay Intelligence',
description: 'Stop guessing. Start grading.',
type: 'website',
},
};
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
rel="stylesheet"
/>
</head>
<body className="min-h-screen antialiased">
<nav className="fixed top-0 w-full z-50 border-b border-[var(--border)] bg-[var(--bg)]/80 backdrop-blur-md">
<div className="max-w-6xl mx-auto px-4 h-16 flex items-center justify-between">
<a href="/" className="font-mono font-bold text-xl tracking-tight">
Beton<span className="text-[var(--accent)]">BLK</span>
</a>
<div className="flex items-center gap-6 text-sm">
<a href="/blog" className="text-[var(--text-muted)] hover:text-white transition">Blog</a>
<a href="/scan" className="text-[var(--text-muted)] hover:text-white transition">Scan</a>
<a href="/tracker" className="text-[var(--text-muted)] hover:text-white transition">Tracker</a>
<a href="/login" className="px-4 py-2 rounded-lg bg-[var(--accent)] text-white text-sm font-medium hover:opacity-90 transition">
Log In
</a>
</div>
</div>
</nav>
<main className="pt-16">{children}</main>
</body>
</html>
);
}
+61
View File
@@ -0,0 +1,61 @@
'use client';
import { useState } from 'react';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
// TODO: Integrate with Supabase Auth
// const { error } = await supabase.auth.signInWithPassword({ email, password });
setLoading(false);
setError('Auth integration pending. Backend is ready.');
};
return (
<section className="min-h-[80vh] flex items-center justify-center px-4">
<div className="w-full max-w-sm">
<h1 className="text-3xl font-bold text-center mb-8">Log In</h1>
<form onSubmit={handleLogin} className="space-y-4">
<div>
<label className="block text-sm text-[var(--text-muted)] mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 rounded-xl bg-[var(--card)] border border-[var(--border)] text-white placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent)]"
/>
</div>
<div>
<label className="block text-sm text-[var(--text-muted)] mb-1">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="w-full px-4 py-3 rounded-xl bg-[var(--card)] border border-[var(--border)] text-white placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent)]"
/>
</div>
{error && <p className="text-[var(--grade-d)] text-sm">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-[var(--accent)] text-white rounded-xl font-medium hover:opacity-90 transition disabled:opacity-50"
>
{loading ? 'Logging in...' : 'Log In'}
</button>
</form>
<p className="text-center text-sm text-[var(--text-muted)] mt-6">
Don't have an account? <a href="/signup" className="text-[var(--accent)] hover:underline">Sign up</a>
</p>
</div>
</section>
);
}
+17
View File
@@ -0,0 +1,17 @@
import Hero from '@/components/Hero';
import HowItWorks from '@/components/HowItWorks';
import Features from '@/components/Features';
import Pricing from '@/components/Pricing';
import Footer from '@/components/Footer';
export default function Home() {
return (
<>
<Hero />
<HowItWorks />
<Features />
<Pricing />
<Footer />
</>
);
}
+63
View File
@@ -0,0 +1,63 @@
'use client';
import { useState } from 'react';
export default function SignupPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
// TODO: Integrate with Supabase Auth
// const { error } = await supabase.auth.signUp({ email, password });
setLoading(false);
setError('Auth integration pending. Backend is ready.');
};
return (
<section className="min-h-[80vh] flex items-center justify-center px-4">
<div className="w-full max-w-sm">
<h1 className="text-3xl font-bold text-center mb-2">Create Account</h1>
<p className="text-center text-[var(--text-muted)] text-sm mb-8">5 free scans. No credit card required.</p>
<form onSubmit={handleSignup} className="space-y-4">
<div>
<label className="block text-sm text-[var(--text-muted)] mb-1">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="w-full px-4 py-3 rounded-xl bg-[var(--card)] border border-[var(--border)] text-white placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent)]"
/>
</div>
<div>
<label className="block text-sm text-[var(--text-muted)] mb-1">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
className="w-full px-4 py-3 rounded-xl bg-[var(--card)] border border-[var(--border)] text-white placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent)]"
/>
</div>
{error && <p className="text-[var(--grade-d)] text-sm">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full py-3 bg-[var(--accent)] text-white rounded-xl font-medium hover:opacity-90 transition disabled:opacity-50"
>
{loading ? 'Creating account...' : 'Sign Up — Free'}
</button>
</form>
<p className="text-center text-sm text-[var(--text-muted)] mt-6">
Already have an account? <a href="/login" className="text-[var(--accent)] hover:underline">Log in</a>
</p>
</div>
</section>
);
}