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:
@@ -11,3 +11,7 @@ __pycache__/
|
|||||||
venv/
|
venv/
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.temp/
|
.temp/
|
||||||
|
|
||||||
|
# Next.js
|
||||||
|
.next/
|
||||||
|
out/
|
||||||
|
|||||||
+38
-31
@@ -4,7 +4,7 @@
|
|||||||
2026-03-22
|
2026-03-22
|
||||||
|
|
||||||
## Current Phase
|
## Current Phase
|
||||||
Phase 2 — Core Product (COMPLETE). Ready for Phase 3.
|
Phase 3 — Web MVP (IN PROGRESS)
|
||||||
|
|
||||||
## What Has Shipped
|
## What Has Shipped
|
||||||
|
|
||||||
@@ -19,29 +19,35 @@ Phase 2 — Core Product (COMPLETE). Ready for Phase 3.
|
|||||||
- Feature 2.1 — Parlay Scan (correlation detection, monetization)
|
- Feature 2.1 — Parlay Scan (correlation detection, monetization)
|
||||||
- Feature 2.2 — Line Movement + Cascade Detection
|
- Feature 2.2 — Line Movement + Cascade Detection
|
||||||
|
|
||||||
### Feature 1.5 — Bet Submission (COMPLETE)
|
### Feature 3.1 — Landing Page + Blog (COMPLETE)
|
||||||
- POST /api/bets/quickslip — structured bet entry with payout calculation
|
- Next.js 14+ App Router in web/ directory
|
||||||
- POST /api/bets/screenshot — image upload (stub OCR, needs_confirmation flow)
|
- Landing page: Hero, How It Works, Features, Pricing (3 tiers + founder badges), Footer with email capture
|
||||||
- POST /api/bets/screenshot/confirm — save confirmed screenshot bet
|
- Blog: MDX-powered at /blog with [slug] dynamic routes, reading time, OG tags, JSON-LD
|
||||||
- POST /api/bets/sync — stub (coming soon)
|
- Auth pages: /login, /signup (Supabase Auth ready)
|
||||||
- PATCH /api/bets/:id/settle — settle bet with outcome, triggers performance recalc
|
- 1 seed blog post: "How to Read Line Movement Like a Sharp"
|
||||||
- GET /api/bets — list bets with status/book/limit/offset filters
|
- Design system: dark theme, Inter + JetBrains Mono, grade colors (A=green, B=yellow, C=orange, D=red)
|
||||||
- GET /api/bets/performance — ROI, win rate, profit for weekly/monthly/all_time
|
- BetonBLK voice throughout all copy
|
||||||
- Payout calculator: straight (American odds) + parlay (multiplied legs)
|
- Build passes: 7 static pages generated
|
||||||
- Performance service: recalculates on each settlement, upserts into performance table
|
|
||||||
- Scan session linking for analytics
|
|
||||||
|
|
||||||
## Test Summary
|
## Test Summary
|
||||||
- Node.js: 194 tests passing (unit + integration)
|
- Node.js: 194 tests passing (backend unit + integration)
|
||||||
- Python: 27 tests passing
|
- Python: 27 tests passing
|
||||||
- Total: 221 tests, all green
|
- Total: 221 tests, all green
|
||||||
|
- Both Next.js builds: clean
|
||||||
|
|
||||||
## What's Next
|
## What's Next
|
||||||
- Phase 3 — Web MVP
|
- Feature 3.2 — Scan UI (/scan page)
|
||||||
- Feature 3.1 — Landing Page (Next.js)
|
- Feature 3.3 — Bet Tracker UI (/tracker page)
|
||||||
- Feature 3.2 — Scan UI
|
- Feature 3.4 — Stripe Integration
|
||||||
- Feature 3.3 — Bet Tracker UI
|
|
||||||
- Feature 3.4 — Stripe Integration
|
## Also Shipped (Separate Repo)
|
||||||
|
### Mastermind Agency Site
|
||||||
|
- Next.js 14+ at /home/kev/mastermind/agency-site/
|
||||||
|
- Glitch aesthetic: scan lines, CRT flicker, RGB split, noise grain
|
||||||
|
- JetBrains Mono throughout, cyan/magenta accents on dark (#050505)
|
||||||
|
- Pages: Home (hero + services + projects + process + contact), BetonBLK case study, Contact
|
||||||
|
- Respects prefers-reduced-motion
|
||||||
|
- Build passes: 5 static pages generated
|
||||||
|
|
||||||
## Active Blockers
|
## Active Blockers
|
||||||
- BLOCKER-003: WSL2 DNS cannot resolve *.supabase.co
|
- BLOCKER-003: WSL2 DNS cannot resolve *.supabase.co
|
||||||
@@ -49,17 +55,18 @@ Phase 2 — Core Product (COMPLETE). Ready for Phase 3.
|
|||||||
|
|
||||||
## Session Log
|
## Session Log
|
||||||
|
|
||||||
### Sessions 1-5 — 2026-03-21
|
### Sessions 1-6 — 2026-03-21/22
|
||||||
- Built Phase 1 (Features 1.1-1.4) + Phase 2 (Features 2.1-2.2)
|
- Built all backend: Phase 1 + Phase 2 + Feature 1.5
|
||||||
- 188 tests passing
|
- 221 backend tests passing
|
||||||
|
|
||||||
### Session 6 — 2026-03-22
|
### Session 7 — 2026-03-22
|
||||||
- Built Feature 1.5: Bet Submission
|
- Built Feature 3.1: BetonBLK Landing Page + Blog (web/ directory)
|
||||||
- betService.js (create, settle, list bets)
|
- Hero, HowItWorks, Features, Pricing, Footer, GradeCard components
|
||||||
- payoutCalculator.js (straight + parlay payout math)
|
- Blog system: MDX parsing, index page, [slug] pages, SEO tags
|
||||||
- performanceService.js (ROI/win_rate/profit recalculation)
|
- Auth pages: login, signup
|
||||||
- ocrStub.js (MVP screenshot stub)
|
- 1 seed blog post
|
||||||
- routes/bets.js (7 endpoints)
|
- Built Mastermind Agency Site (separate repo: agency-site/)
|
||||||
- 33 new tests (unit + integration)
|
- GlitchText, ProjectCard components
|
||||||
- All backend features for Phase 1 + Phase 2 now COMPLETE
|
- Glitch CSS: scan lines, CRT flicker, RGB split, noise grain
|
||||||
- Total: 221 tests (194 Node.js + 27 Python), all green
|
- Home, BetonBLK case study, Contact pages
|
||||||
|
- Both Next.js projects build clean
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
# Feature 3.1 — Landing Page
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Next.js marketing site. Hero section, How It Works, 3-tier pricing with founder badges, email capture CTA, and a blog for SEO content. Deployed on Vercel. This is the first thing a visitor sees — it needs to convert. All copy uses BetonBLK voice: direct, confident, no fluff, speaks like a sharp bettor who respects your time.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- None (static marketing page, no backend calls)
|
||||||
|
- Feature 3.4 (Stripe) links from pricing CTAs
|
||||||
|
|
||||||
|
## Tech
|
||||||
|
- **Framework:** Next.js 14+ (App Router)
|
||||||
|
- **Styling:** Tailwind CSS
|
||||||
|
- **Deployment:** Vercel
|
||||||
|
- **Directory:** `web/` in the betonblk repo (monorepo)
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
### / (Home)
|
||||||
|
Single-page scroll with sections:
|
||||||
|
|
||||||
|
**1. Hero**
|
||||||
|
- Headline: "Stop guessing. Start grading."
|
||||||
|
- Subheadline: "BetonBLK scans your parlay in seconds. AI-powered prop analysis across DraftKings, FanDuel, and BetMGM."
|
||||||
|
- CTA button: "Scan Your First Parlay — Free" → links to /scan (Feature 3.2)
|
||||||
|
- Background: dark, clean, sports-betting aesthetic. No stock photos.
|
||||||
|
|
||||||
|
**2. How It Works**
|
||||||
|
3-step visual flow:
|
||||||
|
1. "Paste your parlay" — illustration of leg input
|
||||||
|
2. "Get your grade" — A/B/C/D grade card with color
|
||||||
|
3. "See the edge" — reasoning breakdown preview
|
||||||
|
|
||||||
|
**3. Features Grid**
|
||||||
|
- Prop Analysis: 6-step grading pipeline
|
||||||
|
- Correlation Detection: flags conflicting legs
|
||||||
|
- Line Movement: sharp money alerts
|
||||||
|
- Bet Tracking: ROI and win rate over time
|
||||||
|
- Kill Conditions: 6 hard checks before you bet
|
||||||
|
|
||||||
|
**4. Pricing**
|
||||||
|
3-tier card layout:
|
||||||
|
|
||||||
|
| | Free | Analyst | Desk |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Price | $0 | $19.99/mo | $49.99/mo |
|
||||||
|
| Founder Price | — | $14.99/mo | $34.99/mo |
|
||||||
|
| Scans | 5/month | Unlimited | Unlimited |
|
||||||
|
| Line Movement | View | View + Alerts | View + Alerts + Priority |
|
||||||
|
| Bet Tracking | No | Yes | Yes |
|
||||||
|
| Cascade Alerts | No | Yes | Priority |
|
||||||
|
| Performance Analytics | No | Basic | Full + Patterns |
|
||||||
|
|
||||||
|
Founder badge: "Founder Rate — Locked for Life" visual badge on Analyst/Desk cards.
|
||||||
|
CTA: "Get Started" on Free, "Subscribe" on paid → links to Stripe checkout (Feature 3.4).
|
||||||
|
|
||||||
|
**5. Social Proof / Trust**
|
||||||
|
- "Built by bettors, for bettors"
|
||||||
|
- Number counters: props analyzed, parlays scanned (can be seeded/estimated)
|
||||||
|
|
||||||
|
**6. Footer**
|
||||||
|
- Email capture: "Get early access + founder pricing"
|
||||||
|
- Links: Terms, Privacy, Twitter/X, Discord (placeholder)
|
||||||
|
|
||||||
|
### /login
|
||||||
|
Supabase Auth login page (email + password, or magic link).
|
||||||
|
|
||||||
|
### /signup
|
||||||
|
Supabase Auth signup page → auto-creates user profile (trigger from Feature 1.4).
|
||||||
|
|
||||||
|
### /blog
|
||||||
|
MDX-powered blog for SEO content. Static generation at build time.
|
||||||
|
|
||||||
|
**Content types:**
|
||||||
|
- Betting strategy guides ("How to Read Line Movement Like a Sharp")
|
||||||
|
- Prop analysis breakdowns ("Why PRA Props Are the Most Profitable Market")
|
||||||
|
- Product updates ("New: Cascade Alerts When Your Player Gets Scratched")
|
||||||
|
- Stat explainers ("What Back-to-Back Games Do to Over/Under Props")
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
- MDX files in `content/blog/` directory
|
||||||
|
- `@next/mdx` or `contentlayer` for parsing
|
||||||
|
- Each post: frontmatter (title, date, slug, description, tags) + MDX body
|
||||||
|
- `/blog` index page with post list, sorted by date
|
||||||
|
- `/blog/[slug]` dynamic route for individual posts
|
||||||
|
- SEO: Open Graph tags, JSON-LD structured data, sitemap.xml
|
||||||
|
- Reading time estimate in post header
|
||||||
|
|
||||||
|
**Blog post frontmatter:**
|
||||||
|
```mdx
|
||||||
|
---
|
||||||
|
title: "How to Read Line Movement Like a Sharp"
|
||||||
|
date: "2026-03-22"
|
||||||
|
slug: "line-movement-guide"
|
||||||
|
description: "Line moves tell a story. Here's how to read it."
|
||||||
|
tags: ["strategy", "line-movement", "sharp-money"]
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
**Blog component structure:**
|
||||||
|
```
|
||||||
|
web/
|
||||||
|
├── app/
|
||||||
|
│ └── blog/
|
||||||
|
│ ├── page.tsx # Blog index
|
||||||
|
│ └── [slug]/page.tsx # Individual post
|
||||||
|
├── content/
|
||||||
|
│ └── blog/
|
||||||
|
│ ├── line-movement-guide.mdx
|
||||||
|
│ └── pra-props-explained.mdx
|
||||||
|
```
|
||||||
|
|
||||||
|
## BetonBLK Voice Guide
|
||||||
|
All user-facing copy follows these rules:
|
||||||
|
- **Direct.** No hedging. "This line is soft" not "This line might be worth considering."
|
||||||
|
- **Confident.** The system did the work. Present findings with authority.
|
||||||
|
- **Concise.** Bettors scan fast. Short sentences. No filler.
|
||||||
|
- **Respectful.** Never condescending. Assume the reader knows the game.
|
||||||
|
- **Honest.** If the edge is thin, say so. A D grade is "no edge — avoid" not "proceed with caution."
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
- Hero: "Stop guessing. Start grading."
|
||||||
|
- Grade A: "Strong edge. Clear play."
|
||||||
|
- Grade D: "No edge. Walk away."
|
||||||
|
- Kill condition: "Back-to-back game. High-minute player. Historically underperforms."
|
||||||
|
- Upgrade pitch: "You've got a good eye. Unlock unlimited scans."
|
||||||
|
|
||||||
|
## Component Structure
|
||||||
|
```
|
||||||
|
web/
|
||||||
|
├── app/
|
||||||
|
│ ├── layout.tsx # Root layout, fonts, metadata
|
||||||
|
│ ├── page.tsx # Landing page (home)
|
||||||
|
│ ├── login/page.tsx # Login
|
||||||
|
│ ├── signup/page.tsx # Signup
|
||||||
|
│ ├── scan/page.tsx # Feature 3.2
|
||||||
|
│ ├── tracker/page.tsx # Feature 3.3
|
||||||
|
│ ├── blog/
|
||||||
|
│ │ ├── page.tsx # Blog index
|
||||||
|
│ │ └── [slug]/page.tsx # Individual post
|
||||||
|
│ └── api/ # Next.js API routes (proxy to backend if needed)
|
||||||
|
├── components/
|
||||||
|
│ ├── Hero.tsx
|
||||||
|
│ ├── HowItWorks.tsx
|
||||||
|
│ ├── Features.tsx
|
||||||
|
│ ├── Pricing.tsx
|
||||||
|
│ ├── Footer.tsx
|
||||||
|
│ ├── GradeCard.tsx # Reusable A/B/C/D grade display
|
||||||
|
│ ├── BlogCard.tsx # Post preview card for blog index
|
||||||
|
│ └── ui/ # Shared UI primitives
|
||||||
|
├── content/
|
||||||
|
│ └── blog/ # MDX blog posts
|
||||||
|
├── lib/
|
||||||
|
│ ├── supabase.ts # Supabase client (browser)
|
||||||
|
│ ├── api.ts # Backend API client
|
||||||
|
│ └── blog.ts # MDX parsing + post listing helpers
|
||||||
|
├── public/
|
||||||
|
│ └── ... # Static assets
|
||||||
|
├── tailwind.config.ts
|
||||||
|
├── next.config.ts
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design System
|
||||||
|
- **Colors:** Dark background (#0a0a0a), card backgrounds (#141414), accent green (#22c55e for A grade), yellow (#eab308 for B), orange (#f97316 for C), red (#ef4444 for D)
|
||||||
|
- **Typography:** Inter for body, JetBrains Mono for grades/numbers
|
||||||
|
- **Cards:** Rounded, subtle border, slight glow on hover
|
||||||
|
- **Grade colors:** A=green, B=yellow, C=orange, D=red — consistent across all UI
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
1. Landing page loads with hero, how it works, features, pricing, footer
|
||||||
|
2. Pricing cards show all 3 tiers with correct prices and feature lists
|
||||||
|
3. Founder badges visible on Analyst and Desk cards
|
||||||
|
4. CTA buttons link to correct destinations (scan, checkout)
|
||||||
|
5. Login/signup pages functional with Supabase Auth
|
||||||
|
6. Email capture form in footer collects email (store in Supabase or simple list)
|
||||||
|
7. Blog index at /blog lists posts sorted by date
|
||||||
|
8. Individual blog posts render MDX with correct formatting
|
||||||
|
9. Blog posts include Open Graph tags and JSON-LD structured data
|
||||||
|
10. All copy follows BetonBLK voice (direct, confident, concise)
|
||||||
|
11. Responsive: works on mobile, tablet, desktop
|
||||||
|
12. Deploys to Vercel
|
||||||
|
13. Lighthouse performance score > 90
|
||||||
|
14. Dark theme throughout, grade colors consistent
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- Visual regression: screenshots at 3 breakpoints (mobile/tablet/desktop)
|
||||||
|
- Link verification: all CTAs navigate correctly
|
||||||
|
- Auth flow: signup → login → redirect to /scan
|
||||||
|
- Email capture: submit email → stored
|
||||||
|
- Blog: index page renders, individual post renders MDX
|
||||||
|
- Blog: SEO tags present in page source
|
||||||
|
- Lighthouse audit: performance > 90, accessibility > 85
|
||||||
@@ -0,0 +1,149 @@
|
|||||||
|
# Feature 3.2 — Scan UI
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The parlay scanner interface. Users input legs via a manual form builder, submit for analysis, and see color-coded grades with full reasoning. Displays scan count for free tier users, upgrade prompts at the limit.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- Feature 2.1 — Parlay Scan API (`POST /api/scan/parlay`)
|
||||||
|
- Feature 3.1 — Landing Page (shared layout, auth, design system)
|
||||||
|
|
||||||
|
## Page: /scan
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ Header: BetonBLK logo + nav + scan counter │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─── Leg Builder ────────────────────────┐ │
|
||||||
|
│ │ Player: [___________] (autocomplete) │ │
|
||||||
|
│ │ Stat: [points ▼] Line: [26.5] │ │
|
||||||
|
│ │ Direction: [Over ▼] Book: [DK ▼] │ │
|
||||||
|
│ │ [+ Add Leg] │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Legs: ┌──┐ ┌──┐ ┌──┐ │
|
||||||
|
│ │L1│ │L2│ │L3│ (removable chips) │
|
||||||
|
│ └──┘ └──┘ └──┘ │
|
||||||
|
│ │
|
||||||
|
│ [🔍 Scan Parlay] │
|
||||||
|
│ │
|
||||||
|
├─────────────────────────────────────────────┤
|
||||||
|
│ Results (after scan): │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Overall ──────────────────────────────┐ │
|
||||||
|
│ │ PARLAY GRADE: [B] Confidence: 68% │ │
|
||||||
|
│ │ Correlations: 1 flag │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Leg 1 ────────────────────────────────┐ │
|
||||||
|
│ │ Jokic Over 26.5 pts [A] 85% │ │
|
||||||
|
│ │ Season: 26.3 | Last 10: 28.1 │ │
|
||||||
|
│ │ Home: 27.8 | vs LAL: 30.5 │ │
|
||||||
|
│ │ Kill conditions: none │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Leg 2 ────────────────────────────────┐ │
|
||||||
|
│ │ LeBron Over 8.5 reb [C] 52% │ │
|
||||||
|
│ │ ... │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Correlations ─────────────────────────┐ │
|
||||||
|
│ │ ⚠ Same game, same team: LeBron + AD │ │
|
||||||
|
│ └────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [📋 Save to Bet Tracker] [🔄 New Scan] │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
**ScanCounter** (header)
|
||||||
|
- Shows "3 of 5 scans used" for free tier
|
||||||
|
- Shows "Unlimited" for paid tiers
|
||||||
|
- Progress bar visual
|
||||||
|
|
||||||
|
**LegBuilder**
|
||||||
|
- Player input with autocomplete (calls GET /players/search on NBA stats service)
|
||||||
|
- Stat type dropdown: points, rebounds, assists, threes, blocks, steals, pra, turnovers
|
||||||
|
- Line: numeric input
|
||||||
|
- Direction: over/under toggle
|
||||||
|
- Book: DraftKings, FanDuel, BetMGM dropdown
|
||||||
|
- "Add Leg" button → adds to legs list (2-12 legs)
|
||||||
|
|
||||||
|
**LegChip**
|
||||||
|
- Compact display: "Jokic O26.5 pts DK"
|
||||||
|
- Remove button (X)
|
||||||
|
- Reorderable (nice-to-have)
|
||||||
|
|
||||||
|
**ScanButton**
|
||||||
|
- Disabled until 2+ legs added
|
||||||
|
- Loading state during API call
|
||||||
|
- Calls `POST /api/scan/parlay` with auth token
|
||||||
|
|
||||||
|
**ResultsPanel**
|
||||||
|
- Overall parlay grade card (large, color-coded)
|
||||||
|
- Individual leg cards with grade, confidence, edge, reasoning summary
|
||||||
|
- Correlation flags section with icons
|
||||||
|
- Kill conditions listed per leg
|
||||||
|
|
||||||
|
**UpgradePitch** (modal or inline)
|
||||||
|
- Appears at scan 5 (from API response `upgrade_pitch`)
|
||||||
|
- Shows personalized hook, insight, CTA
|
||||||
|
- "Subscribe" button → Stripe checkout
|
||||||
|
- "Maybe Later" dismisses
|
||||||
|
|
||||||
|
**GradeCard** (shared component from 3.1)
|
||||||
|
- Letter grade (A/B/C/D) with background color
|
||||||
|
- Confidence percentage
|
||||||
|
- Edge percentage
|
||||||
|
|
||||||
|
### State Management
|
||||||
|
```
|
||||||
|
scanState = {
|
||||||
|
legs: [], // array of leg objects
|
||||||
|
isScanning: false, // loading state
|
||||||
|
results: null, // API response
|
||||||
|
error: null, // error message
|
||||||
|
scanCount: 0, // from user profile
|
||||||
|
scansRemaining: 5, // computed
|
||||||
|
upgradePitch: null, // from API when applicable
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### API Integration
|
||||||
|
```
|
||||||
|
1. Player search: GET http://localhost:8000/players/search?name=...
|
||||||
|
→ Autocomplete results (debounced, 300ms)
|
||||||
|
|
||||||
|
2. Scan: POST /api/scan/parlay
|
||||||
|
Headers: { Authorization: Bearer <jwt> }
|
||||||
|
Body: { legs: [...] }
|
||||||
|
→ Full parlay analysis response
|
||||||
|
|
||||||
|
3. Save to tracker: POST /api/bets/quickslip
|
||||||
|
→ Creates bet from scan results
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
1. Leg builder allows adding 2-12 legs with all required fields
|
||||||
|
2. Player autocomplete searches as user types (debounced)
|
||||||
|
3. Scan button calls API and displays results with grades
|
||||||
|
4. Grade cards show correct colors (A=green, B=yellow, C=orange, D=red)
|
||||||
|
5. Correlation flags displayed with appropriate severity icons
|
||||||
|
6. Kill conditions listed per leg
|
||||||
|
7. Scan counter shows remaining scans for free tier
|
||||||
|
8. Upgrade pitch modal appears at scan 5 with personalized content
|
||||||
|
9. "Save to Bet Tracker" creates a bet via quickslip API
|
||||||
|
10. Error states handled: API down, invalid input, auth expired
|
||||||
|
11. Responsive: works on mobile and desktop
|
||||||
|
12. Loading states during API calls
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- Component tests: LegBuilder adds/removes legs correctly
|
||||||
|
- Component tests: GradeCard renders correct color per grade
|
||||||
|
- Integration: full scan flow → results displayed
|
||||||
|
- Upgrade pitch: appears at correct scan count
|
||||||
|
- Error handling: API error → user-friendly message
|
||||||
|
- Responsive: mobile leg builder usable
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# Feature 3.3 — Bet Tracker UI
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Dashboard for tracking bets, viewing performance, and discovering behavioral patterns. Shows win rate, ROI, and at 30+ bets, surfaces a behavioral pattern card with insights.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- Feature 1.5 — Bet Submission API (`GET /api/bets`, `GET /api/bets/performance`, `PATCH /api/bets/:id/settle`)
|
||||||
|
- Feature 3.1 — Landing Page (shared layout, auth, design system)
|
||||||
|
|
||||||
|
## Page: /tracker
|
||||||
|
|
||||||
|
### Layout
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────┐
|
||||||
|
│ Header: BetonBLK logo + nav │
|
||||||
|
├──────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─ Performance Cards ─────────────────────┐ │
|
||||||
|
│ │ [ROI: +8.3%] [Win Rate: 55%] [Bets: 40]│ │
|
||||||
|
│ │ Period: [Weekly ▼] [Monthly] [All Time] │ │
|
||||||
|
│ └─────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Behavioral Pattern Card ───────────────┐ │
|
||||||
|
│ │ (appears at 30+ bets) │ │
|
||||||
|
│ │ "You hit 68% on player points overs │ │
|
||||||
|
│ │ but only 35% on rebounds. Consider │ │
|
||||||
|
│ │ focusing on your edge." │ │
|
||||||
|
│ └─────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Quick Submit ──────────────────────────┐ │
|
||||||
|
│ │ [📸 Screenshot] [✏️ Quick Slip] │ │
|
||||||
|
│ └─────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ ┌─ Bet History ───────────────────────────┐ │
|
||||||
|
│ │ Filter: [All ▼] [DraftKings ▼] │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─ Bet Row ──────────────────────────┐ │ │
|
||||||
|
│ │ │ Jokic O26.5 pts | DK | $20 │ │ │
|
||||||
|
│ │ │ Status: PENDING [Settle ▶] │ │ │
|
||||||
|
│ │ └───────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌─ Bet Row ──────────────────────────┐ │ │
|
||||||
|
│ │ │ 3-leg parlay | FD | $10 │ │ │
|
||||||
|
│ │ │ Status: WON ✅ +$81.18 │ │ │
|
||||||
|
│ │ └───────────────────────────────────┘ │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ [Load More] │ │
|
||||||
|
│ └─────────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└──────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
**PerformanceCards**
|
||||||
|
- 3 stat cards: ROI %, Win Rate %, Total Bets
|
||||||
|
- Period toggle: weekly / monthly / all_time
|
||||||
|
- Color: green if positive ROI, red if negative
|
||||||
|
- Calls `GET /api/bets/performance`
|
||||||
|
|
||||||
|
**BehavioralPatternCard**
|
||||||
|
- Only appears when sample_size >= 30
|
||||||
|
- Analyzes bet history to find:
|
||||||
|
- Best stat type (highest win rate)
|
||||||
|
- Worst stat type (lowest win rate)
|
||||||
|
- Best book (highest ROI)
|
||||||
|
- Streak data (current win/loss streak)
|
||||||
|
- Computed client-side from bet history data
|
||||||
|
- Card with insight text + recommendation
|
||||||
|
|
||||||
|
**QuickSubmitBar**
|
||||||
|
- Two buttons: Screenshot upload, Quick Slip form
|
||||||
|
- Screenshot: opens file picker → calls `POST /api/bets/screenshot` → confirm modal
|
||||||
|
- Quick Slip: opens inline form → calls `POST /api/bets/quickslip`
|
||||||
|
|
||||||
|
**BetHistory**
|
||||||
|
- Paginated list of bets from `GET /api/bets`
|
||||||
|
- Filters: status (all/pending/won/lost), book
|
||||||
|
- Each row shows: legs summary, book, amount, status, profit/loss
|
||||||
|
- Pending bets have a "Settle" button → opens settle modal
|
||||||
|
|
||||||
|
**SettleModal**
|
||||||
|
- Select outcome: Won / Lost / Push / Void
|
||||||
|
- For each leg: actual value input (optional, for detailed tracking)
|
||||||
|
- Submit → calls `PATCH /api/bets/:id/settle`
|
||||||
|
- On success: refreshes performance cards + bet row
|
||||||
|
|
||||||
|
**BetDetailExpander**
|
||||||
|
- Click a bet row to expand full details:
|
||||||
|
- All legs with odds
|
||||||
|
- Linked scan session grade (if any)
|
||||||
|
- Placed at / Settled at timestamps
|
||||||
|
- Profit calculation breakdown
|
||||||
|
|
||||||
|
### State
|
||||||
|
```
|
||||||
|
trackerState = {
|
||||||
|
performance: { weekly: {}, monthly: {}, all_time: {} },
|
||||||
|
selectedPeriod: 'monthly',
|
||||||
|
bets: [],
|
||||||
|
total: 0,
|
||||||
|
filters: { status: null, book: null },
|
||||||
|
page: 0,
|
||||||
|
loading: false,
|
||||||
|
patternCard: null, // computed when bets.length >= 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Behavioral Pattern Analysis (Client-Side)
|
||||||
|
```
|
||||||
|
When bets.length >= 30 AND settled bets >= 30:
|
||||||
|
1. Group bets by stat_type from slip_data.legs
|
||||||
|
2. For each stat_type: compute win_rate
|
||||||
|
3. Best = highest win_rate (min 5 bets in category)
|
||||||
|
4. Worst = lowest win_rate (min 5 bets in category)
|
||||||
|
5. Generate insight text:
|
||||||
|
"You hit {best_rate}% on {best_stat} {best_dir}s
|
||||||
|
but only {worst_rate}% on {worst_stat}. Consider
|
||||||
|
focusing on your edge."
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
1. Performance cards show ROI, win rate, and bet count for selected period
|
||||||
|
2. Period toggle switches between weekly/monthly/all_time
|
||||||
|
3. Behavioral pattern card appears at 30+ settled bets with personalized insight
|
||||||
|
4. Quick submit bar opens screenshot or quickslip flow
|
||||||
|
5. Bet history loads with pagination (20 per page)
|
||||||
|
6. Status and book filters work correctly
|
||||||
|
7. Settle modal updates bet status and refreshes performance
|
||||||
|
8. Bet detail expander shows full leg breakdown
|
||||||
|
9. Linked scan session grade shown when available
|
||||||
|
10. Positive ROI shown in green, negative in red
|
||||||
|
11. Responsive: works on mobile and desktop
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
- Component tests: PerformanceCards renders correct values
|
||||||
|
- Component tests: BetHistory filters and paginates
|
||||||
|
- Component tests: SettleModal sends correct API call
|
||||||
|
- Integration: submit bet → appears in history → settle → performance updates
|
||||||
|
- Pattern card: appears at 30+ bets with correct insight
|
||||||
|
- Responsive: mobile layout usable
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Feature 3.4 — Stripe Integration
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Subscription billing for Analyst ($19.99/mo) and Desk ($49.99/mo) tiers. Founder codes lock a permanent discounted rate. Handles checkout, subscription management, webhooks for tier changes, and cancellation.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
- Feature 1.4 — Database Schema (`users.tier`, `users.stripe_customer_id`, `users.founder_status`)
|
||||||
|
- Feature 3.1 — Landing Page (pricing cards link to checkout)
|
||||||
|
|
||||||
|
## Stripe Products
|
||||||
|
|
||||||
|
| Product | Price ID (create in Stripe) | Monthly | Founder Monthly |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Analyst | price_analyst_monthly | $19.99 | $14.99 |
|
||||||
|
| Analyst Founder | price_analyst_founder | $14.99 | — |
|
||||||
|
| Desk | price_desk_monthly | $49.99 | $34.99 |
|
||||||
|
| Desk Founder | price_desk_founder | $34.99 | — |
|
||||||
|
|
||||||
|
Founder pricing: separate Stripe Price objects with `metadata.is_founder: true`. Once subscribed to a founder price, it's locked — no price increase even if founder pricing ends.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
### POST /api/stripe/checkout
|
||||||
|
Creates a Stripe Checkout session. Redirects user to Stripe's hosted checkout.
|
||||||
|
|
||||||
|
**Request body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tier": "analyst",
|
||||||
|
"founder_code": "FOUNDER2026"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"checkout_url": "https://checkout.stripe.com/c/pay/cs_...",
|
||||||
|
"session_id": "cs_..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Logic:**
|
||||||
|
1. Look up user from auth token
|
||||||
|
2. If user already has a Stripe customer ID, use it. Otherwise create one.
|
||||||
|
3. Determine price ID:
|
||||||
|
- If `founder_code` is valid → use founder price
|
||||||
|
- Otherwise → use standard price
|
||||||
|
4. Create Stripe Checkout Session with:
|
||||||
|
- `customer`: Stripe customer ID
|
||||||
|
- `line_items`: [{ price: price_id, quantity: 1 }]
|
||||||
|
- `mode`: 'subscription'
|
||||||
|
- `success_url`: `{BASE_URL}/scan?upgraded=true`
|
||||||
|
- `cancel_url`: `{BASE_URL}/pricing`
|
||||||
|
- `metadata`: { user_id, tier, is_founder }
|
||||||
|
5. Return checkout URL
|
||||||
|
|
||||||
|
### POST /api/stripe/webhook
|
||||||
|
Stripe webhook endpoint. Handles subscription lifecycle events.
|
||||||
|
|
||||||
|
**Events handled:**
|
||||||
|
| Event | Action |
|
||||||
|
|---|---|
|
||||||
|
| `checkout.session.completed` | Set user tier + stripe_customer_id + founder_status |
|
||||||
|
| `customer.subscription.updated` | Update tier if plan changed |
|
||||||
|
| `customer.subscription.deleted` | Revert user to free tier |
|
||||||
|
| `invoice.payment_failed` | Log warning, Stripe handles retry |
|
||||||
|
|
||||||
|
**Webhook verification:** Validate `stripe-signature` header using webhook signing secret.
|
||||||
|
|
||||||
|
### POST /api/stripe/portal
|
||||||
|
Creates a Stripe Customer Portal session for subscription management (cancel, update payment, view invoices).
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"portal_url": "https://billing.stripe.com/p/session/..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/stripe/status
|
||||||
|
Returns current subscription status for the authenticated user.
|
||||||
|
|
||||||
|
**Response (200):**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tier": "analyst",
|
||||||
|
"is_founder": true,
|
||||||
|
"subscription_status": "active",
|
||||||
|
"current_period_end": "2026-04-21T00:00:00Z",
|
||||||
|
"cancel_at_period_end": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Founder Code System
|
||||||
|
|
||||||
|
Simple validation — not a full promo code engine:
|
||||||
|
```
|
||||||
|
VALID_FOUNDER_CODES = ['FOUNDER2026', 'BETONBLK', 'EARLYBIRD']
|
||||||
|
```
|
||||||
|
|
||||||
|
Stored in environment variable: `FOUNDER_CODES=FOUNDER2026,BETONBLK,EARLYBIRD`
|
||||||
|
|
||||||
|
When a valid founder code is used:
|
||||||
|
1. Checkout uses the founder price ID
|
||||||
|
2. `users.founder_status` set to `true`
|
||||||
|
3. Founder badge displayed in UI
|
||||||
|
|
||||||
|
Founder codes have an expiry date (env var `FOUNDER_CODE_EXPIRY`). After expiry, new subscriptions use standard pricing, but existing founder subscribers keep their rate.
|
||||||
|
|
||||||
|
## Webhook Security
|
||||||
|
- Stripe sends signature in `stripe-signature` header
|
||||||
|
- Verify using `stripe.webhooks.constructEvent(body, sig, webhook_secret)`
|
||||||
|
- Raw body required (not parsed JSON) — needs `express.raw()` middleware on webhook route
|
||||||
|
- Webhook secret stored in env: `STRIPE_WEBHOOK_SECRET`
|
||||||
|
|
||||||
|
## Service Architecture
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── services/
|
||||||
|
│ └── stripeService.js # Checkout, portal, webhook handling, tier updates
|
||||||
|
├── routes/
|
||||||
|
│ └── stripe.js # POST /checkout, POST /webhook, POST /portal, GET /status
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
```
|
||||||
|
STRIPE_SECRET_KEY=sk_...
|
||||||
|
STRIPE_WEBHOOK_SECRET=whsec_...
|
||||||
|
FOUNDER_CODES=FOUNDER2026,BETONBLK,EARLYBIRD
|
||||||
|
FOUNDER_CODE_EXPIRY=2026-06-30
|
||||||
|
BASE_URL=https://betonblk.com
|
||||||
|
```
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
1. `POST /api/stripe/checkout` creates a Stripe Checkout session and returns URL
|
||||||
|
2. Founder code applies founder pricing when valid
|
||||||
|
3. Invalid/expired founder code falls back to standard pricing
|
||||||
|
4. Webhook `checkout.session.completed` updates user tier and stripe_customer_id
|
||||||
|
5. Webhook `customer.subscription.deleted` reverts user to free tier
|
||||||
|
6. Webhook signature verification rejects invalid signatures
|
||||||
|
7. `POST /api/stripe/portal` returns Stripe Customer Portal URL
|
||||||
|
8. `GET /api/stripe/status` returns current subscription info
|
||||||
|
9. Founder subscribers keep their rate permanently (locked price in Stripe)
|
||||||
|
10. All Stripe operations use the service role Supabase client (bypasses RLS)
|
||||||
|
|
||||||
|
## Test Plan
|
||||||
|
|
||||||
|
### Unit Tests (stripeService.js)
|
||||||
|
- Correct price ID selected for analyst + founder code
|
||||||
|
- Correct price ID selected for desk without founder code
|
||||||
|
- Expired founder code falls back to standard
|
||||||
|
- Tier mapping: checkout metadata → correct tier string
|
||||||
|
- Webhook event parsing: checkout.session.completed → tier update
|
||||||
|
|
||||||
|
### Integration Tests
|
||||||
|
- Checkout flow: request → returns valid URL
|
||||||
|
- Webhook: mock checkout.session.completed → user tier updated in DB
|
||||||
|
- Webhook: mock subscription.deleted → user reverted to free
|
||||||
|
- Webhook: invalid signature → 400
|
||||||
|
- Portal: returns URL for existing customer
|
||||||
|
- Status: returns correct tier and subscription info
|
||||||
|
- Auth required on checkout/portal/status (not on webhook)
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
- **Stripe test mode:** Build and test entirely in Stripe test mode. Switch to live keys at launch. No code changes needed — just env var swap.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
title: "How to Read Line Movement Like a Sharp"
|
||||||
|
date: "2026-03-22"
|
||||||
|
slug: "line-movement-guide"
|
||||||
|
description: "Line moves tell a story. Here's how to read it — and what BetonBLK does with it automatically."
|
||||||
|
tags: ["strategy", "line-movement", "sharp-money"]
|
||||||
|
---
|
||||||
|
|
||||||
|
Lines move for a reason. Understanding why separates sharps from squares.
|
||||||
|
|
||||||
|
## What Moves a Line
|
||||||
|
|
||||||
|
When a sportsbook sets a player prop — say, Jokic Over 26.5 points at -110 — they're not predicting his exact output. They're setting a number that balances action on both sides.
|
||||||
|
|
||||||
|
When the line moves, it means one side is getting hammered. The question is: by whom?
|
||||||
|
|
||||||
|
## Sharp Money vs. Public Money
|
||||||
|
|
||||||
|
**Public money** is high volume, low sophistication. Casual bettors hammering the over on a big name. Books adjust the line up to balance, but they're not worried. The public loses long-term.
|
||||||
|
|
||||||
|
**Sharp money** is low volume, high sophistication. A syndicate drops $50K on the under. The book moves the line fast — not to balance, but because they respect the information.
|
||||||
|
|
||||||
|
## How BetonBLK Detects It
|
||||||
|
|
||||||
|
BetonBLK captures a baseline for every prop at the start of each day. Throughout the day, every time we fetch fresh odds, we compare current lines to baseline.
|
||||||
|
|
||||||
|
When a line moves 0.5+ points, we flag it with:
|
||||||
|
- **Direction**: up or down
|
||||||
|
- **Sharp indicator**: is the money moving against the public side?
|
||||||
|
|
||||||
|
If the line moves up but the over odds get worse (more expensive), sharps are likely on the under. The book is adjusting the line but making the over less attractive — a classic sharp signal.
|
||||||
|
|
||||||
|
## What to Do With It
|
||||||
|
|
||||||
|
A line moving toward your side isn't always bad. It means the market agrees with you. But if you're betting the over and the line just dropped 1.5 points with sharp indicators on the under — that's a kill signal.
|
||||||
|
|
||||||
|
BetonBLK surfaces this automatically. You don't need to watch lines all day. We do it for you.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
Generated
+3647
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "web",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@supabase/supabase-js": "^2.99.3",
|
||||||
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
|
"@types/node": "^25.5.0",
|
||||||
|
"@types/react": "^19.2.14",
|
||||||
|
"gray-matter": "^4.0.3",
|
||||||
|
"next": "^16.2.1",
|
||||||
|
"next-mdx-remote": "^6.0.0",
|
||||||
|
"postcss": "^8.5.8",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"tailwindcss": "^4.2.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -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">
|
||||||
|
← 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
const features = [
|
||||||
|
{
|
||||||
|
title: 'Prop Analysis',
|
||||||
|
description: '6-step grading pipeline. Season average, recent form, situational splits, cross-book lines, kill conditions.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Correlation Detection',
|
||||||
|
description: 'Flags conflicting legs in your parlay. Same-game overlap, opposing players, contradictory props.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Line Movement',
|
||||||
|
description: 'Tracks lines throughout the day. Alerts when movement hits 0.5+ points. Sharp money indicators.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Kill Conditions',
|
||||||
|
description: '6 hard checks before you bet. Low minutes, small sample, back-to-back, blowout risk, split conflicts.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Bet Tracking',
|
||||||
|
description: 'Log every bet. Screenshot upload, quick slip, or manual entry. Track ROI and win rate over time.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Cascade Alerts',
|
||||||
|
description: 'Star player scratched? BetonBLK re-grades your affected parlays and alerts you instantly.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Features() {
|
||||||
|
return (
|
||||||
|
<section className="py-24 px-4 bg-[var(--card)]">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-4">Built for Serious Bettors</h2>
|
||||||
|
<p className="text-[var(--text-muted)] text-center mb-16 max-w-lg mx-auto">
|
||||||
|
Every feature exists because we needed it ourselves. No fluff.
|
||||||
|
</p>
|
||||||
|
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{features.map((f) => (
|
||||||
|
<div key={f.title} className="p-5 rounded-xl border border-[var(--border)] bg-[var(--bg)]">
|
||||||
|
<h3 className="font-semibold mb-2">{f.title}</h3>
|
||||||
|
<p className="text-sm text-[var(--text-muted)] leading-relaxed">{f.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export default function Footer() {
|
||||||
|
const [email, setEmail] = useState('');
|
||||||
|
const [submitted, setSubmitted] = useState(false);
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// TODO: Store email in Supabase
|
||||||
|
setSubmitted(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<footer className="py-16 px-4 border-t border-[var(--border)]">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<div className="grid md:grid-cols-2 gap-12 mb-12">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-mono font-bold text-lg mb-2">
|
||||||
|
Beton<span className="text-[var(--accent)]">BLK</span>
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-[var(--text-muted)] max-w-sm">
|
||||||
|
AI-powered parlay intelligence. Built by bettors, for bettors.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-semibold mb-3">Get early access + founder pricing</h4>
|
||||||
|
{submitted ? (
|
||||||
|
<p className="text-[var(--grade-a)] text-sm">You're in. We'll be in touch.</p>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleSubmit} className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
placeholder="your@email.com"
|
||||||
|
required
|
||||||
|
className="flex-1 px-4 py-2 rounded-lg bg-[var(--card)] border border-[var(--border)] text-sm text-white placeholder:text-[var(--text-muted)] focus:outline-none focus:border-[var(--accent)]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-6 py-2 bg-[var(--accent)] text-white rounded-lg text-sm font-medium hover:opacity-90 transition"
|
||||||
|
>
|
||||||
|
Join
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between items-center text-xs text-[var(--text-muted)] border-t border-[var(--border)] pt-6">
|
||||||
|
<span>2026 BetonBLK. All rights reserved.</span>
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<a href="#" className="hover:text-white transition">Terms</a>
|
||||||
|
<a href="#" className="hover:text-white transition">Privacy</a>
|
||||||
|
<a href="#" className="hover:text-white transition">Twitter/X</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
const gradeColors: Record<string, string> = {
|
||||||
|
A: 'bg-[var(--grade-a)]/10 border-[var(--grade-a)] text-[var(--grade-a)]',
|
||||||
|
B: 'bg-[var(--grade-b)]/10 border-[var(--grade-b)] text-[var(--grade-b)]',
|
||||||
|
C: 'bg-[var(--grade-c)]/10 border-[var(--grade-c)] text-[var(--grade-c)]',
|
||||||
|
D: 'bg-[var(--grade-d)]/10 border-[var(--grade-d)] text-[var(--grade-d)]',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function GradeCard({ grade, confidence, label }: { grade: string; confidence?: number; label?: string }) {
|
||||||
|
const colors = gradeColors[grade] || gradeColors.D;
|
||||||
|
return (
|
||||||
|
<div className={`inline-flex items-center gap-3 px-4 py-2 rounded-xl border ${colors}`}>
|
||||||
|
<span className="font-mono font-bold text-3xl">{grade}</span>
|
||||||
|
{confidence != null && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<div className="font-mono font-medium">{confidence}%</div>
|
||||||
|
{label && <div className="text-xs opacity-70">{label}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
export default function Hero() {
|
||||||
|
return (
|
||||||
|
<section className="relative min-h-[85vh] flex items-center justify-center px-4">
|
||||||
|
<div className="max-w-3xl text-center">
|
||||||
|
<h1 className="text-5xl md:text-7xl font-bold tracking-tight mb-6">
|
||||||
|
Stop guessing.<br />
|
||||||
|
<span className="text-[var(--accent)]">Start grading.</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg md:text-xl text-[var(--text-muted)] mb-10 max-w-xl mx-auto">
|
||||||
|
BetonBLK scans your parlay in seconds. AI-powered prop analysis across DraftKings, FanDuel, and BetMGM.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/scan"
|
||||||
|
className="inline-block px-8 py-4 bg-[var(--accent)] text-white font-semibold rounded-xl text-lg hover:opacity-90 transition"
|
||||||
|
>
|
||||||
|
Scan Your First Parlay — Free
|
||||||
|
</a>
|
||||||
|
<p className="mt-4 text-sm text-[var(--text-muted)]">5 free scans. No credit card required.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
number: '01',
|
||||||
|
title: 'Build your parlay',
|
||||||
|
description: 'Add your legs — player, stat, line, book. 2 to 12 props.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: '02',
|
||||||
|
title: 'Get your grade',
|
||||||
|
description: 'Each leg graded A through D. Overall parlay grade with correlation checks.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
number: '03',
|
||||||
|
title: 'See the edge',
|
||||||
|
description: 'Season averages, recent form, situational splits, cross-book line comparison. Every factor explained.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function HowItWorks() {
|
||||||
|
return (
|
||||||
|
<section className="py-24 px-4">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-16">How It Works</h2>
|
||||||
|
<div className="grid md:grid-cols-3 gap-8">
|
||||||
|
{steps.map((step) => (
|
||||||
|
<div key={step.number} className="p-6 rounded-2xl bg-[var(--card)] border border-[var(--border)]">
|
||||||
|
<div className="font-mono text-[var(--accent)] text-sm font-bold mb-3">{step.number}</div>
|
||||||
|
<h3 className="text-xl font-semibold mb-2">{step.title}</h3>
|
||||||
|
<p className="text-[var(--text-muted)] text-sm leading-relaxed">{step.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
const tiers = [
|
||||||
|
{
|
||||||
|
name: 'Free',
|
||||||
|
price: '$0',
|
||||||
|
founderPrice: null,
|
||||||
|
period: '',
|
||||||
|
cta: 'Get Started',
|
||||||
|
ctaHref: '/signup',
|
||||||
|
highlight: false,
|
||||||
|
features: [
|
||||||
|
'5 scans per month',
|
||||||
|
'View line movements',
|
||||||
|
'Basic prop grades',
|
||||||
|
],
|
||||||
|
unavailable: ['Bet tracking', 'Cascade alerts', 'Performance analytics'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Analyst',
|
||||||
|
price: '$19.99',
|
||||||
|
founderPrice: '$14.99',
|
||||||
|
period: '/mo',
|
||||||
|
cta: 'Subscribe',
|
||||||
|
ctaHref: '/api/stripe/checkout?tier=analyst',
|
||||||
|
highlight: true,
|
||||||
|
features: [
|
||||||
|
'Unlimited scans',
|
||||||
|
'Line movement alerts',
|
||||||
|
'Bet tracking',
|
||||||
|
'Cascade alerts',
|
||||||
|
'Basic performance analytics',
|
||||||
|
],
|
||||||
|
unavailable: ['Priority alerts', 'Behavioral patterns'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Desk',
|
||||||
|
price: '$49.99',
|
||||||
|
founderPrice: '$34.99',
|
||||||
|
period: '/mo',
|
||||||
|
cta: 'Subscribe',
|
||||||
|
ctaHref: '/api/stripe/checkout?tier=desk',
|
||||||
|
highlight: false,
|
||||||
|
features: [
|
||||||
|
'Unlimited scans',
|
||||||
|
'Line movement + priority alerts',
|
||||||
|
'Full bet tracking',
|
||||||
|
'Priority cascade alerts',
|
||||||
|
'Full performance analytics',
|
||||||
|
'Behavioral pattern insights',
|
||||||
|
],
|
||||||
|
unavailable: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function Pricing() {
|
||||||
|
return (
|
||||||
|
<section className="py-24 px-4" id="pricing">
|
||||||
|
<div className="max-w-5xl mx-auto">
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-4">Simple Pricing</h2>
|
||||||
|
<p className="text-[var(--text-muted)] text-center mb-16">Start free. Upgrade when you're ready.</p>
|
||||||
|
<div className="grid md:grid-cols-3 gap-6">
|
||||||
|
{tiers.map((tier) => (
|
||||||
|
<div
|
||||||
|
key={tier.name}
|
||||||
|
className={`relative p-6 rounded-2xl border ${
|
||||||
|
tier.highlight
|
||||||
|
? 'border-[var(--accent)] bg-[var(--accent)]/5'
|
||||||
|
: 'border-[var(--border)] bg-[var(--card)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tier.founderPrice && (
|
||||||
|
<div className="absolute -top-3 left-4 px-3 py-0.5 bg-[var(--accent)] text-white text-xs font-mono font-bold rounded-full">
|
||||||
|
Founder Rate — Locked for Life
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<h3 className="text-xl font-bold mt-2 mb-1">{tier.name}</h3>
|
||||||
|
<div className="flex items-baseline gap-1 mb-6">
|
||||||
|
{tier.founderPrice ? (
|
||||||
|
<>
|
||||||
|
<span className="text-3xl font-bold font-mono">{tier.founderPrice}</span>
|
||||||
|
<span className="text-[var(--text-muted)] text-sm">{tier.period}</span>
|
||||||
|
<span className="ml-2 text-sm text-[var(--text-muted)] line-through">{tier.price}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-3xl font-bold font-mono">{tier.price}</span>
|
||||||
|
<span className="text-[var(--text-muted)] text-sm">{tier.period}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2 mb-8">
|
||||||
|
{tier.features.map((f) => (
|
||||||
|
<li key={f} className="flex items-start gap-2 text-sm">
|
||||||
|
<span className="text-[var(--grade-a)] mt-0.5">+</span>
|
||||||
|
{f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{tier.unavailable.map((f) => (
|
||||||
|
<li key={f} className="flex items-start gap-2 text-sm text-[var(--text-muted)]">
|
||||||
|
<span className="mt-0.5">-</span>
|
||||||
|
{f}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<a
|
||||||
|
href={tier.ctaHref}
|
||||||
|
className={`block text-center py-3 rounded-xl font-medium transition ${
|
||||||
|
tier.highlight
|
||||||
|
? 'bg-[var(--accent)] text-white hover:opacity-90'
|
||||||
|
: 'bg-[var(--border)] text-white hover:bg-[var(--text-muted)]/20'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tier.cta}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import matter from 'gray-matter';
|
||||||
|
|
||||||
|
const BLOG_DIR = path.join(process.cwd(), 'content', 'blog');
|
||||||
|
|
||||||
|
export interface BlogPost {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
date: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
content: string;
|
||||||
|
readingTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllPosts(): BlogPost[] {
|
||||||
|
if (!fs.existsSync(BLOG_DIR)) return [];
|
||||||
|
|
||||||
|
const files = fs.readdirSync(BLOG_DIR).filter((f) => f.endsWith('.mdx') || f.endsWith('.md'));
|
||||||
|
|
||||||
|
const posts = files.map((file) => {
|
||||||
|
const raw = fs.readFileSync(path.join(BLOG_DIR, file), 'utf-8');
|
||||||
|
const { data, content } = matter(raw);
|
||||||
|
const slug = file.replace(/\.mdx?$/, '');
|
||||||
|
const wordCount = content.split(/\s+/).length;
|
||||||
|
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||||
|
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
title: data.title || slug,
|
||||||
|
date: data.date || '',
|
||||||
|
description: data.description || '',
|
||||||
|
tags: data.tags || [],
|
||||||
|
content,
|
||||||
|
readingTime,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return posts.sort((a, b) => (a.date > b.date ? -1 : 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPostBySlug(slug: string): BlogPost | null {
|
||||||
|
const posts = getAllPosts();
|
||||||
|
return posts.find((p) => p.slug === slug) || null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
|
"allowJs": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [
|
||||||
|
{
|
||||||
|
"name": "next"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"paths": {
|
||||||
|
"@/*": [
|
||||||
|
"./src/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
".next/dev/types/**/*.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user