import { useState, useEffect } from "react";
import { Check, Edit2, X, ChevronDown, ChevronUp, ArrowLeft, Plus, Trash2, Lock, Search } from "lucide-react";
import { BarChart, Bar, XAxis, Tooltip, ResponsiveContainer, Cell } from "recharts";
/* ─── FONTS & CSS ──────────────────────────────────────────────────────── */
const Fonts = () => (
);
const GlobalCSS = () => (
);
/* ─── LOGO (Road Traveled) ─────────────────────────────────────────────── */
function Logo({ size = 34 }) {
const W = size * 1.7;
return (
{[[3,27,"#1C5FA8"],[14,18,"#1A6B3A"],[26,12,"#7030A0"]].map(([x,y,c],i) => (
))}
);
}
/* ─── CONSTANTS ────────────────────────────────────────────────────────── */
const FREE_LIMIT = 10;
const PRICE_MO = 9.99;
const PRICE_YR = 79.99;
const SAVE_PCT = Math.round((1 - PRICE_YR / (PRICE_MO * 12)) * 100);
const ADMIN_PW = "visitall2025"; // ← change before deploying
const TIERS = [
{ min:0, max:499, label:"Newcomer", emoji:"🌱", color:"#6B7A99", bg:"#F0F4FA" },
{ min:500, max:1999, label:"Explorer", emoji:"🥾", color:"#1A6B3A", bg:"#E6F5ED" },
{ min:2000, max:4999, label:"Adventurer", emoji:"🗺️", color:"#1C5FA8", bg:"#EBF4FF" },
{ min:5000, max:9999, label:"Trailblazer", emoji:"⛰️", color:"#7030A0", bg:"#F3EAFE" },
{ min:10000, max:24999, label:"Legend", emoji:"🏆", color:"#C2470A", bg:"#FFF0E6" },
{ min:25000, max:Infinity,label:"Icon", emoji:"🌟", color:"#8B6914", bg:"#FFFBE6" },
];
const CAT_BASE_PTS = { states:100, parks:150, arenas:75, landmarks:200 };
const ITEM_OVERRIDES = {
"states:Alaska":300, "states:Hawaii":300,
"states:North Dakota":150, "states:Wyoming":130,
"parks:Denali":400, "parks:Gates of the Arctic":500,
"parks:Kobuk Valley":450, "parks:Wrangell-St. Elias":450,
"parks:Dry Tortugas":350, "parks:Lake Clark":380,
"parks:Isle Royale":280,
"landmarks:Statue of Liberty":350, "landmarks:Mount Rushmore":300,
"landmarks:Golden Gate Bridge":250, "landmarks:Antelope Canyon":280,
"landmarks:The Wave":350,
};
const SEED_LB = [
{ u:"ParkRangerPete", pts:12340, states:5, parks:60, arenas:0, land:3 },
{ u:"NationalParkNancy", pts:10890, states:12, parks:45, arenas:2, land:5 },
{ u:"RoadTripRaj", pts:9210, states:42, parks:8, arenas:5, land:6 },
{ u:"WanderlustWendy", pts:8760, states:18, parks:20, arenas:4, land:10 },
{ u:"AlexTheExplorer", pts:7890, states:35, parks:15, arenas:10, land:8 },
{ u:"TrailblazerTom", pts:7140, states:30, parks:12, arenas:8, land:4 },
{ u:"CoastToCoastCora", pts:6120, states:50, parks:3, arenas:2, land:1 },
{ u:"SportsFanSteve", pts:5340, states:20, parks:5, arenas:30, land:3 },
{ u:"LandmarkLucy", pts:4870, states:8, parks:3, arenas:1, land:18 },
{ u:"ArenaAce", pts:4120, states:15, parks:2, arenas:28, land:2 },
{ u:"BackpackingBen", pts:3450, states:22, parks:10, arenas:3, land:2 },
{ u:"StateCrusherSam", pts:2980, states:28, parks:2, arenas:1, land:0 },
{ u:"TravelingTara", pts:1870, states:14, parks:4, arenas:2, land:1 },
{ u:"FirstTimerFrank", pts:890, states:7, parks:1, arenas:0, land:0 },
];
const DEFAULT_SUBS = {
states: [{id:"night",l:"Spent a Night",e:"🌙"},{id:"drove",l:"Drove Through",e:"🚗"},{id:"concert",l:"Been to a Concert",e:"🎵"},{id:"meal",l:"Ate a Local Meal",e:"🍽️"},{id:"sign",l:"Photo with Welcome Sign",e:"📸"}],
parks: [{id:"hiked",l:"Hiked a Trail",e:"🥾"},{id:"camped",l:"Camped Overnight",e:"⛺"},{id:"stamp",l:"Got Passport Stamp",e:"📮"},{id:"sunrise",l:"Watched Sunrise/Sunset",e:"🌅"},{id:"wildlife",l:"Spotted Wildlife",e:"🦌"}],
arenas: [{id:"game",l:"Attended a Live Game",e:"🎟️"},{id:"food",l:"Stadium Food & Drink",e:"🌭"},{id:"photo",l:"Photo Outside Venue",e:"📸"},{id:"win",l:"Witnessed a Win",e:"🏆"},{id:"merch",l:"Bought Team Merch",e:"👕"}],
landmarks: [{id:"photo",l:"Got the Iconic Shot",e:"📸"},{id:"toured",l:"Toured the Inside",e:"🎟️"},{id:"history",l:"Learned the History",e:"📚"},{id:"golden",l:"Visited at Golden Hour",e:"🌅"},{id:"shared",l:"Brought Someone Here",e:"🤝"}],
};
const INIT_CUSTOM = {
"states:California": [{id:"gg",l:"Walk Golden Gate Bridge",e:"🌉"},{id:"disney",l:"Visit Disneyland",e:"🎢"}],
"states:New York": [{id:"liberty",l:"Visit Statue of Liberty",e:"🗽"},{id:"broadway",l:"See a Broadway Show",e:"🎭"}],
"states:Texas": [{id:"alamo",l:"Visit the Alamo",e:"⚔️"},{id:"bbq",l:"Authentic TX BBQ",e:"🍖"}],
"states:Hawaii": [{id:"volcano",l:"See Active Volcano",e:"🌋"},{id:"surf",l:"Try Surfing",e:"🏄"}],
"parks:Yellowstone": [{id:"faithful",l:"See Old Faithful Erupt",e:"♨️"},{id:"prismatic",l:"Grand Prismatic Spring",e:"🎨"}],
"parks:Yosemite": [{id:"halfdome",l:"Half Dome at Sunrise",e:"🌄"},{id:"falls",l:"Stand Under Yosemite Falls",e:"💦"}],
};
const CATEGORIES = [
{ id:"states", emoji:"🗺️", title:"All 50 States", color:"#1C5FA8", light:"#EBF4FF",
desc:"Every state is a new chapter — visit the capital, try a local meal, snap the welcome sign.",
items:["Alabama","Alaska","Arizona","Arkansas","California","Colorado","Connecticut","Delaware","Florida","Georgia","Hawaii","Idaho","Illinois","Indiana","Iowa","Kansas","Kentucky","Louisiana","Maine","Maryland","Massachusetts","Michigan","Minnesota","Mississippi","Missouri","Montana","Nebraska","Nevada","New Hampshire","New Jersey","New Mexico","New York","North Carolina","North Dakota","Ohio","Oklahoma","Oregon","Pennsylvania","Rhode Island","South Carolina","South Dakota","Tennessee","Texas","Utah","Vermont","Virginia","Washington","West Virginia","Wisconsin","Wyoming"] },
{ id:"parks", emoji:"🌲", title:"National Parks", color:"#1A6B3A", light:"#E6F5ED",
desc:"All 63 official NPS parks — from Yellowstone to the Everglades. Hike the trails, earn the stamps.",
items:["Acadia","Arches","Badlands","Big Bend","Biscayne","Black Canyon","Bryce Canyon","Canyonlands","Capitol Reef","Carlsbad Caverns","Channel Islands","Congaree","Crater Lake","Cuyahoga Valley","Death Valley","Denali","Dry Tortugas","Everglades","Gates of the Arctic","Gateway Arch","Glacier","Grand Canyon","Grand Teton","Great Basin","Great Sand Dunes","Great Smoky Mountains","Guadalupe Mountains","Haleakalā","Hawaii Volcanoes","Hot Springs","Indiana Dunes","Isle Royale","Joshua Tree","Katmai","Kenai Fjords","Kings Canyon","Kobuk Valley","Lake Clark","Lassen Volcanic","Mammoth Cave","Mesa Verde","Mount Rainier","North Cascades","Olympic","Petrified Forest","Pinnacles","Redwood","Rocky Mountain","Saguaro","Sequoia","Shenandoah","Theodore Roosevelt","Virgin Islands","Voyageurs","White Sands","Wind Cave","Wrangell-St. Elias","Yellowstone","Yosemite","Zion"] },
{ id:"arenas", emoji:"🏟️", title:"Sports Arenas", color:"#7030A0", light:"#F3EAFE",
desc:"NFL, NBA, MLB, and NHL — sit in the stands of every home venue across America's four major leagues.",
leagues:[
{id:"nfl",name:"NFL",emoji:"🏈",items:["Arrowhead Stadium (KC)","Lambeau Field (GB)","SoFi Stadium (LAR)","MetLife Stadium (NYG/NYJ)","AT&T Stadium (DAL)","Levi's Stadium (SF)","Gillette Stadium (NE)","Lucas Oil Stadium (IND)","M&T Bank Stadium (BAL)","Bank of America Stadium (CAR)","Hard Rock Stadium (MIA)","Soldier Field (CHI)","U.S. Bank Stadium (MIN)","State Farm Stadium (ARI)","Mercedes-Benz Stadium (ATL)","Nissan Stadium (TEN)","FirstEnergy Stadium (CLE)","Ford Field (DET)","Paycor Stadium (CIN)","Acrisure Stadium (PIT)"]},
{id:"nba",name:"NBA",emoji:"🏀",items:["TD Garden (BOS)","Madison Square Garden (NY)","Chase Center (GS)","Crypto.com Arena (LAL)","United Center (CHI)","Barclays Center (BKN)","Ball Arena (DEN)","Fiserv Forum (MIL)","Kaseya Center (MIA)","American Airlines Center (DAL)","Spectrum Center (CHA)","Footprint Center (PHX)","Target Center (MIN)","Gainbridge Fieldhouse (IND)","State Farm Arena (ATL)","Rocket Mortgage FieldHouse (CLE)","Little Caesars Arena (DET)","FedEx Forum (MEM)","Moda Center (POR)","Golden 1 Center (SAC)"]},
{id:"mlb",name:"MLB",emoji:"⚾",items:["Fenway Park (BOS)","Wrigley Field (CHC)","Yankee Stadium (NY)","Dodger Stadium (LA)","Oracle Park (SF)","Camden Yards (BAL)","Busch Stadium (STL)","Globe Life Field (TEX)","Coors Field (COL)","PNC Park (PIT)","Petco Park (SD)","Minute Maid Park (HOU)","Truist Park (ATL)","Nationals Park (WSH)","Great American Ball Park (CIN)","Progressive Field (CLE)","Kauffman Stadium (KC)","Target Field (MIN)","American Family Field (MIL)","Guaranteed Rate Field (CWS)"]},
{id:"nhl",name:"NHL",emoji:"🏒",items:["Bell Centre (MTL)","United Center (CHI)","Madison Square Garden (NY)","TD Garden (BOS)","PPG Paints Arena (PIT)","Amalie Arena (TB)","Enterprise Center (STL)","Rogers Arena (VAN)","Xcel Energy Center (MIN)","Scotiabank Arena (TOR)","Wells Fargo Center (PHI)","Little Caesars Arena (DET)","Bridgestone Arena (NSH)","Ball Arena (COL)","SAP Center (SJS)","Gila River Arena (ARI)","KeyBank Center (BUF)","PNC Arena (CAR)","Nationwide Arena (CBJ)","Acrisure Arena (PS)"]},
]},
{ id:"landmarks", emoji:"🗽", title:"US Landmarks", color:"#C2470A", light:"#FFF0E6",
desc:"Statue of Liberty, Golden Gate, Mount Rushmore — the places that define America.",
items:["Statue of Liberty","Golden Gate Bridge","Mount Rushmore","Grand Canyon","Lincoln Memorial","Space Needle","Hollywood Sign","Niagara Falls","Times Square","Hoover Dam","Gateway Arch","Independence Hall","Graceland","Alcatraz Island","Pearl Harbor","The Alamo","Antelope Canyon","Multnomah Falls","Monument Valley","Zion Narrows","Old Faithful","Horseshoe Bend","Delicate Arch","The Wave","Devils Tower","Natural Bridges","Mesa Verde","Cape Hatteras Lighthouse","Crater Lake Rim","Bryce Amphitheater"] },
];
/* ─── HELPERS ──────────────────────────────────────────────────────────── */
const getTier = (pts) => TIERS.find(t => pts >= t.min && pts <= t.max) || TIERS[0];
const getMainPts = (pv, catId, item) => pv[`${catId}:${item}`] ?? pv[`${catId}:default`] ?? CAT_BASE_PTS[catId] ?? 100;
const getSubPts = (main) => Math.max(5, Math.round(main * 0.25 / 5) * 5);
const ikey = (c, i) => `${c}:${i}`;
const fmt = (n) => Number(n).toLocaleString();
function calcCatPts(catId, checks, pv, customSubs) {
let total = 0;
const entries = Object.entries(checks);
for (let i = 0; i < entries.length; i++) {
const [key, data] = entries[i];
if (!data || !data.main || !key.startsWith(catId + ":")) continue;
const parts = key.split(":");
const item = parts.slice(1).join(":");
const mp = getMainPts(pv, catId, item);
const sp = getSubPts(mp);
total += mp;
const defs = DEFAULT_SUBS[catId] || [];
const cust = customSubs[key] || [];
const allSubs = defs.concat(cust);
for (let j = 0; j < allSubs.length; j++) {
if (data.subs && data.subs[allSubs[j].id]) total += sp;
}
}
return total;
}
function calcStats(checks, pv, customSubs) {
const counts = { states:0, parks:0, arenas:0, land:0 };
const entries = Object.entries(checks);
for (let i = 0; i < entries.length; i++) {
const [key, data] = entries[i];
if (!data || !data.main) continue;
const catId = key.split(":")[0];
if (catId === "states") counts.states++;
if (catId === "parks") counts.parks++;
if (catId === "arenas") counts.arenas++;
if (catId === "landmarks") counts.land++;
}
const pts = ["states","parks","arenas","landmarks"].reduce(
(sum, c) => sum + calcCatPts(c, checks, pv, customSubs), 0
);
return { pts, ...counts };
}
function getAchievements(stats) {
const tot = stats.states + stats.parks + stats.arenas + stats.land;
return [
{ id:"first", emoji:"🎉", label:"First Check", done: tot >= 1 },
{ id:"s5", emoji:"🗺️", label:"5 States", done: stats.states >= 5 },
{ id:"s25", emoji:"🌎", label:"25 States", done: stats.states >= 25 },
{ id:"s50", emoji:"🦅", label:"All 50 States", done: stats.states >= 50 },
{ id:"p5", emoji:"🌲", label:"5 Parks", done: stats.parks >= 5 },
{ id:"p20", emoji:"🏕️", label:"20 Parks", done: stats.parks >= 20 },
{ id:"p63", emoji:"🌿", label:"All 63 Parks", done: stats.parks >= 63 },
{ id:"a10", emoji:"🏟️", label:"10 Arenas", done: stats.arenas >= 10 },
{ id:"l10", emoji:"🗽", label:"10 Landmarks", done: stats.land >= 10 },
{ id:"c500", emoji:"🔥", label:"500 Points", done: stats.pts >= 500 },
{ id:"c2k", emoji:"⭐", label:"2,000 Points", done: stats.pts >= 2000 },
{ id:"c10k", emoji:"🏆", label:"10,000 Points", done: stats.pts >= 10000 },
{ id:"slam", emoji:"🎯", label:"Grand Slam", done: stats.states>=1 && stats.parks>=1 && stats.arenas>=1 && stats.land>=1 },
];
}
/* ─── SMALL COMPONENTS ─────────────────────────────────────────────────── */
function Bubble({ checked, color, size = 22 }) {
return (
{checked && }
);
}
function TierBadge({ pts, small }) {
const t = getTier(pts);
return (
{t.emoji} {t.label}
);
}
function PtsBadge({ pts, small }) {
return (
🪙 {fmt(pts)}
);
}
/* ─── JOIN SCREEN ──────────────────────────────────────────────────────── */
function JoinScreen({ onJoin }) {
const [name, setName] = useState("");
return (
VISIT THEM ALL
by Own All 50
Track every state, park, arena and landmark. Earn points. Climb the global leaderboard.
YOUR USERNAME
setName(e.target.value)}
onKeyDown={e => { if (e.key === "Enter" && name.trim()) onJoin(name.trim()); }}
style={{ marginBottom: 16, fontSize: 15 }}
/>
name.trim() && onJoin(name.trim())}>
Start My Journey →
First 10 checks free · No credit card needed
);
}
/* ─── ADMIN PASSWORD MODAL ─────────────────────────────────────────────── */
function AdminLoginModal({ onClose, onSuccess }) {
const [pw, setPw] = useState("");
const [error, setError] = useState(false);
function attempt() {
if (pw === ADMIN_PW) { onSuccess(); }
else { setError(true); setPw(""); }
}
return (
e.stopPropagation()}
style={{ maxWidth: 360, width: "100%", padding: "36px 32px", textAlign: "center" }}>
🔧
Admin Access
Enter your admin password to unlock the point editor.
{ setPw(e.target.value); setError(false); }}
onKeyDown={e => { if (e.key === "Enter") attempt(); }}
style={{ marginBottom: 8, textAlign: "center", letterSpacing: 4, fontSize: 16 }}
/>
{error && (
Incorrect password. Try again.
)}
Unlock Admin
Cancel
);
}
/* ─── PAYWALL MODAL ────────────────────────────────────────────────────── */
function PaywallModal({ onClose, onPay, used }) {
const [cycle, setCycle] = useState("yearly");
const price = cycle === "monthly" ? PRICE_MO : (PRICE_YR / 12).toFixed(2);
return (
e.stopPropagation()}
style={{ maxWidth: 440, width: "100%", padding: "40px 36px", textAlign: "center" }}>
{used >= FREE_LIMIT ? "Free trial complete!" : `${used} of ${FREE_LIMIT} free checks used`}
Unlock unlimited tracking, points, leaderboard ranking, and custom sub-checks.
{[["monthly","Monthly"], ["yearly", `Yearly – Save ${SAVE_PCT}%`]].map(([v, label]) => (
setCycle(v)} style={{
padding: "8px 20px", borderRadius: 100, border: "none", cursor: "pointer",
fontFamily: "'DM Sans', sans-serif", fontSize: 13, fontWeight: 700,
background: cycle === v ? "white" : "transparent",
color: cycle === v ? "#0A1628" : "#6B7A99",
boxShadow: cycle === v ? "0 2px 6px rgba(0,0,0,.08)" : "none",
transition: "all .2s"
}}>{label}
))}
${price}
/mo
{cycle === "yearly" && (
Billed ${PRICE_YR}/yr · Save {SAVE_PCT}%
)}
{["Unlimited checks across all categories",
"Custom sub-checks for any destination",
"Full leaderboard access and global rank",
"Achievement badges and journey timeline"
].map(f => (
{f}
))}
Unlock Pro — ${cycle === "monthly" ? `${PRICE_MO}/mo` : `${PRICE_YR}/yr`}
{used < FREE_LIMIT ? `Continue free (${FREE_LIMIT - used} checks left)` : "Maybe later"}
);
}
/* ─── DASHBOARD ────────────────────────────────────────────────────────── */
function Dashboard({ user, checks, pv, customSubs, lb }) {
const stats = calcStats(checks, pv, customSubs);
const sorted = [...lb].sort((a, b) => b.pts - a.pts);
const rank = sorted.findIndex(u => u.u === user) + 1;
const earned = getAchievements(stats).filter(a => a.done);
const chartData = [
{ name: "States", pts: calcCatPts("states", checks, pv, customSubs), color: "#1C5FA8" },
{ name: "Parks", pts: calcCatPts("parks", checks, pv, customSubs), color: "#1A6B3A" },
{ name: "Arenas", pts: calcCatPts("arenas", checks, pv, customSubs), color: "#7030A0" },
{ name: "Landmarks", pts: calcCatPts("landmarks", checks, pv, customSubs), color: "#C2470A" },
];
const completionRows = [
{ label:"States", done:stats.states, total:50, color:"#1C5FA8", emoji:"🗺️" },
{ label:"Parks", done:stats.parks, total:63, color:"#1A6B3A", emoji:"🌲" },
{ label:"Arenas", done:stats.arenas, total:124, color:"#7030A0", emoji:"🏟️" },
{ label:"Landmarks", done:stats.land, total:30, color:"#C2470A", emoji:"🗽" },
];
const achievements = getAchievements(stats);
const hasAnyPts = chartData.some(d => d.pts > 0);
return (
{/* Hero card */}
Welcome back,
{user}
{rank > 0 && (
· #{rank} globally
)}
{fmt(stats.pts)}
Total Points
{earned.length > 0 && (
🏅 {earned.length} achievement{earned.length !== 1 ? "s" : ""} earned
)}
{/* Points by category chart */}
POINTS BY CATEGORY
{!hasAnyPts ? (
No points yet — go check off some places!
) : (
[`${fmt(v)} pts`, ""]}
contentStyle={{ borderRadius: 10, border: "none", boxShadow: "0 4px 20px rgba(0,0,0,.12)", fontSize: 13 }}
/>
{chartData.map((d, i) => | )}
|
)}
{/* Completion bars */}
COMPLETION
{completionRows.map(r => {
const pct = Math.round((r.done / r.total) * 100);
return (
{r.emoji} {r.label}
{r.done}/{r.total} · {pct}%
);
})}
{/* Achievements */}
ACHIEVEMENTS
{achievements.map(a => (
{a.emoji}
{a.label}
{a.done && }
))}
);
}
/* ─── SUB-CHECK PANEL ──────────────────────────────────────────────────── */
function SubPanel({ catId, item, color, checks, customSubs, pv, onToggleSub, onAddCustom, onRemoveCustom }) {
const key = ikey(catId, item);
const mp = getMainPts(pv, catId, item);
const sp = getSubPts(mp);
const defs = DEFAULT_SUBS[catId] || [];
const cust = customSubs[key] || [];
const subs = (checks[key] && checks[key].subs) || {};
const done = defs.concat(cust).filter(s => subs[s.id]).length;
const [adding, setAdding] = useState(false);
const [newL, setNewL] = useState("");
const [newE, setNewE] = useState("⭐");
const EMOJIS = ["⭐","🎯","🏆","📍","🎪","🌟","🎉","🗺️","🍕","🏖️","🎨","🚀","💎","🤩","🔥"];
function saveCustom() {
if (!newL.trim()) return;
onAddCustom(catId, item, { l: newL.trim(), e: newE });
setNewL("");
setAdding(false);
}
return (
SUB-CHECKS · each
{done}/{defs.length + cust.length} done
{defs.map(s => (
onToggleSub(catId, item, s.id)}>
{s.e} {s.l}
))}
{cust.length > 0 && (
{item} Specials
{cust.map(s => (
onToggleSub(catId, item, s.id)}>
{s.e} {s.l}
onRemoveCustom(catId, item, s.id)} style={{
background: "none", border: "none", cursor: "pointer",
color: "#CDD3DC", padding: 4, display: "flex"
}}>
))}
)}
{adding ? (
{EMOJIS.map(e => (
setNewE(e)} style={{
width: 28, height: 28, borderRadius: 7, cursor: "pointer", fontSize: 15,
border: `2px solid ${newE === e ? color : "#E8EDF5"}`,
background: newE === e ? `${color}18` : "white"
}}>{e}
))}
setNewL(e.target.value)}
placeholder="e.g. Visit local brewery" style={{ marginBottom: 8 }}
onKeyDown={e => { if (e.key === "Enter") saveCustom(); }}/>
Save
{ setAdding(false); setNewL(""); }}>Cancel
) : (
setAdding(true)} style={{
marginTop: 12, display: "flex", alignItems: "center", gap: 7,
background: "none", border: `1.5px dashed ${color}55`, borderRadius: 10,
padding: "7px 14px", cursor: "pointer", color, fontSize: 13, fontWeight: 700,
fontFamily: "'DM Sans', sans-serif", width: "100%", justifyContent: "center"
}}>
Add Custom Check for {item}
)}
);
}
/* ─── TRACKER ──────────────────────────────────────────────────────────── */
function Tracker({ categories, checks, customSubs, pv, onToggleMain, onToggleSub, onAddCustom, onRemoveCustom, isPaid, onPaywall, mainCount }) {
const [selCat, setSelCat] = useState(null);
const [league, setLeague] = useState("nfl");
const [openItem,setOpenItem]= useState(null);
const [search, setSearch] = useState("");
const cat = selCat ? categories.find(c => c.id === selCat) : null;
const items = !cat ? [] : cat.leagues
? (cat.leagues.find(l => l.id === league) || cat.leagues[0]).items
: cat.items;
const shown = items.filter(i => i.toLowerCase().includes(search.toLowerCase()));
function catProgress(c) {
const its = c.leagues ? (c.leagues.find(l => l.id === league) || c.leagues[0]).items : c.items;
const done = its.filter(i => checks[ikey(c.id, i)] && checks[ikey(c.id, i)].main).length;
return { done, total: its.length };
}
return (
{/* Free trial bar */}
{!isPaid && (
Free Trial: {mainCount}/{FREE_LIMIT} checks used
Unlock Unlimited →
)}
{/* Category grid */}
{!selCat && (
<>
My Journey Tracker
Choose a category. Expand any item to see sub-checks and point values.
{categories.map(c => {
const p = catProgress(c);
const pct = Math.round((p.done / p.total) * 100);
return (
{ setSelCat(c.id); setSearch(""); setOpenItem(null); }}
style={{ padding: "26px", cursor: "pointer", border: "2px solid transparent", transition: "all .25s" }}
onMouseEnter={e => { e.currentTarget.style.borderColor = c.color; e.currentTarget.style.transform = "translateY(-2px)"; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = "transparent"; e.currentTarget.style.transform = "translateY(0)"; }}>
PROGRESS
{p.done}/{p.total} · {pct}%
);
})}
>
)}
{/* Item list */}
{cat && (
{ setSelCat(null); setOpenItem(null); }} style={{
display: "flex", alignItems: "center", gap: 7,
background: "none", border: "none", cursor: "pointer",
color: "#6B7A99", fontSize: 14, fontWeight: 600,
fontFamily: "'DM Sans', sans-serif", marginBottom: 10
}}>
All Categories
{cat.emoji} {cat.title}
{cat.leagues && (
{cat.leagues.map(l => (
{ setLeague(l.id); setOpenItem(null); }} style={{
padding: "6px 14px", borderRadius: 100, cursor: "pointer",
fontFamily: "'DM Sans', sans-serif", fontSize: 12.5, fontWeight: 700,
border: `2px solid ${league === l.id ? cat.color : "#E0E6EF"}`,
background: league === l.id ? cat.color : "white",
color: league === l.id ? "white" : "#6B7A99",
transition: "all .2s"
}}>{l.emoji} {l.name}
))}
)}
setSearch(e.target.value)} style={{ paddingLeft: 36 }}/>
{shown.map(item => {
const key = ikey(cat.id, item);
const checked = checks[key] && checks[key].main;
const mp = getMainPts(pv, cat.id, item);
const defs = DEFAULT_SUBS[cat.id] || [];
const cust = customSubs[key] || [];
const allSubs = defs.concat(cust);
const subDone = allSubs.filter(s => checks[key] && checks[key].subs && checks[key].subs[s.id]).length;
const isOpen = openItem === item;
return (
setOpenItem(isOpen ? null : item)}>
{ e.stopPropagation(); onToggleMain(cat.id, item); }} style={{ marginRight: 8 }}>
{item}
{subDone > 0 && (
{subDone}/{allSubs.length} sub-checks done
)}
{isOpen && (
)}
);
})}
{shown.length === 0 && (
No results for "{search}"
)}
{/* Side info */}
HOW POINTS WORK
Earn {CAT_BASE_PTS[cat.id]} pts for each main check.
Each sub-check adds {getSubPts(CAT_BASE_PTS[cat.id])} pts (25% of main).
Remote locations like Alaska or Denali are worth extra!
{!isPaid && (
Unlock Pro
{FREE_LIMIT - mainCount > 0
? `${FREE_LIMIT - mainCount} free checks remaining.`
: "You've used all free checks."} Go Pro to track everything.
See Pricing →
)}
)}
);
}
/* ─── LEADERBOARD ──────────────────────────────────────────────────────── */
function Leaderboard({ lb, user, userStats }) {
const sorted = [...lb].sort((a, b) => b.pts - a.pts);
const medals = ["🥇", "🥈", "🥉"];
const podium = [sorted[1], sorted[0], sorted[2]];
const heights = [140, 170, 120];
const podPos = [2, 1, 3];
return (
🏆 Global Rankings
Compete with adventurers worldwide. Check off more places to climb the board.
{/* Podium */}
{podium.map((entry, i) => {
if (!entry) return
;
const isMe = entry.u === user;
const t = getTier(entry.pts);
const podPts = isMe ? userStats.pts : entry.pts;
const podColors = ["#C0C0C0", "#F4B942", "#CD7F32"];
return (
{entry.u}{isMe ? " (You)" : ""}
{medals[podPos[i] - 1]}
#{podPos[i]}
{fmt(podPts)} pts
);
})}
{/* Full table */}
{sorted.map((entry, idx) => {
const isMe = entry.u === user;
const t = getTier(entry.pts);
const displayPts = isMe ? userStats.pts : entry.pts;
const displayStates = isMe ? userStats.states : entry.states;
const displayParks = isMe ? userStats.parks : entry.parks;
const displayArenas = isMe ? userStats.arenas : entry.arenas;
const displayLand = isMe ? userStats.land : entry.land;
return (
{idx + 1}
{t.emoji}
{entry.u}{isMe ? " (You)" : ""}
{fmt(displayPts)}
{t.emoji} {t.label}
{displayStates}
{displayParks}
{displayArenas}
{displayLand}
);
})}
);
}
/* ─── ADMIN PANEL ──────────────────────────────────────────────────────── */
function AdminPanel({ pv, onSavePv }) {
const [filter, setFilter] = useState("states");
const [search, setSearch] = useState("");
const [editing, setEditing] = useState(null);
const [editVal, setEditVal] = useState("");
const cat = CATEGORIES.find(c => c.id === filter);
const allItems = cat && cat.leagues ? cat.leagues.flatMap(l => l.items) : (cat ? cat.items : []);
const unique = [...new Set(allItems)];
const shown = unique.filter(i => i.toLowerCase().includes(search.toLowerCase()));
const catDef = pv[`${filter}:default`] || CAT_BASE_PTS[filter] || 100;
function startEdit(item) {
setEditing(item);
setEditVal(String(pv[`${filter}:${item}`] !== undefined ? pv[`${filter}:${item}`] : catDef));
}
function saveEdit(item) {
const val = parseInt(editVal, 10);
if (!isNaN(val) && val > 0) {
const newPv = Object.assign({}, pv, { [`${filter}:${item}`]: val });
onSavePv(newPv);
}
setEditing(null);
}
function resetItem(item) {
const newPv = Object.assign({}, pv);
delete newPv[`${filter}:${item}`];
onSavePv(newPv);
}
return (
Point Editor
Click any row to change its point value. Changes are instant and affect all users.
{CATEGORIES.map(c => (
{ setFilter(c.id); setSearch(""); setEditing(null); }} style={{
background: "none", border: "none", cursor: "pointer",
padding: "12px 20px", fontFamily: "'DM Sans', sans-serif",
fontSize: 14, fontWeight: 600,
color: filter === c.id ? "#FF6B35" : "#6B7A99",
borderBottom: filter === c.id ? "3px solid #FF6B35" : "3px solid transparent",
transition: "all .2s"
}}>{c.emoji} {c.title}
))}
setSearch(e.target.value)} style={{ paddingLeft: 36, maxWidth: 320 }}/>
DESTINATION
MAIN PTS
SUB-CHECK PTS
ACTIONS
{shown.map(item => {
const specificKey = `${filter}:${item}`;
const isCustom = pv[specificKey] !== undefined;
const mp = isCustom ? pv[specificKey] : catDef;
const sp = getSubPts(mp);
const isEdit = editing === item;
return (
{item}
{isCustom && (
CUSTOM
)}
{isEdit ? (
setEditVal(e.target.value)}
style={{ width: 80, padding: "5px 10px", fontSize: 14 }}
autoFocus
onKeyDown={e => { if (e.key === "Enter") saveEdit(item); if (e.key === "Escape") setEditing(null); }}
onBlur={() => saveEdit(item)}
/>
) : (
)}
{isEdit ? (
saveEdit(item)}>
) : (
startEdit(item)}>
)}
{isCustom && (
resetItem(item)} title="Reset to default">
)}
);
})}
Tip: Click ✕ to revert any item to the category default of {catDef} pts. Sub-check values are always 25% of the main, calculated automatically.
);
}
/* ─── MAIN APP ─────────────────────────────────────────────────────────── */
export default function App() {
const [user, setUser] = useState(null);
const [tab, setTab] = useState("dashboard");
const [checks, setChecks] = useState({});
const [customSubs, setCustomSubs] = useState(INIT_CUSTOM);
const [pv, setPv] = useState(() => {
const base = {};
CATEGORIES.forEach(c => { base[`${c.id}:default`] = CAT_BASE_PTS[c.id]; });
return Object.assign({}, ITEM_OVERRIDES, base);
});
const [lb, setLb] = useState(SEED_LB);
const [isPaid, setIsPaid] = useState(false);
const [showPaywall,setShowPaywall]= useState(false);
const [adminMode, setAdminMode] = useState(false);
const [showAdminLogin, setShowAdminLogin] = useState(false);
const [loading, setLoading] = useState(true);
const stats = calcStats(checks, pv, customSubs);
const mainCount = Object.values(checks).filter(c => c && c.main).length;
const canCheck = isPaid || mainCount < FREE_LIMIT;
// Load from storage on mount
useEffect(() => {
(async () => {
try {
const r = await window.storage.get("vta_pointValues", true);
if (r) setPv(JSON.parse(r.value));
} catch (_) {}
try {
const r = await window.storage.get("vta_leaderboard", true);
if (r) setLb(JSON.parse(r.value));
} catch (_) {
try { await window.storage.set("vta_leaderboard", JSON.stringify(SEED_LB), true); } catch (_) {}
}
try {
const r = await window.storage.get("vta_lastUser");
if (r) {
setUser(r.value);
try {
const c = await window.storage.get(`vta_checks:${r.value}`);
if (c) setChecks(JSON.parse(c.value));
} catch (_) {}
}
} catch (_) {}
setLoading(false);
})();
}, []);
// Persist checks + update leaderboard when checks change
useEffect(() => {
if (!user) return;
const s = calcStats(checks, pv, customSubs);
(async () => {
try { await window.storage.set(`vta_checks:${user}`, JSON.stringify(checks)); } catch (_) {}
setLb(prev => {
const entry = { u: user, pts: s.pts, states: s.states, parks: s.parks, arenas: s.arenas, land: s.land };
const updated = prev.filter(e => e.u !== user).concat(entry).sort((a, b) => b.pts - a.pts);
(async () => { try { await window.storage.set("vta_leaderboard", JSON.stringify(updated), true); } catch (_) {} })();
return updated;
});
})();
}, [checks, user]);
async function handleJoin(name) {
setUser(name);
try { await window.storage.set("vta_lastUser", name); } catch (_) {}
try {
const r = await window.storage.get(`vta_checks:${name}`);
if (r) setChecks(JSON.parse(r.value));
} catch (_) {}
setTab("dashboard");
}
function handleToggleMain(catId, item) {
const key = ikey(catId, item);
const current = checks[key] && checks[key].main;
if (!current && !canCheck) { setShowPaywall(true); return; }
setChecks(prev => {
const existing = prev[key] || {};
return Object.assign({}, prev, { [key]: Object.assign({}, existing, { main: !existing.main, subs: existing.subs || {} }) });
});
}
function handleToggleSub(catId, item, subId) {
const key = ikey(catId, item);
setChecks(prev => {
const existing = prev[key] || {};
const oldSubs = existing.subs || {};
return Object.assign({}, prev, { [key]: Object.assign({}, existing, { subs: Object.assign({}, oldSubs, { [subId]: !oldSubs[subId] }) }) });
});
}
function handleAddCustom(catId, item, { l, e }) {
if (!l.trim()) return;
const key = ikey(catId, item);
setCustomSubs(prev => {
const existing = prev[key] || [];
return Object.assign({}, prev, { [key]: existing.concat([{ id: `c-${Date.now()}`, l: l.trim(), e }]) });
});
}
function handleRemoveCustom(catId, item, subId) {
const key = ikey(catId, item);
setCustomSubs(prev => {
const existing = prev[key] || [];
return Object.assign({}, prev, { [key]: existing.filter(s => s.id !== subId) });
});
}
async function handleSavePv(newPv) {
setPv(newPv);
try { await window.storage.set("vta_pointValues", JSON.stringify(newPv), true); } catch (_) {}
}
if (loading) {
return (
);
}
if (!user) {
return (
);
}
const NAV_TABS = [
{ id:"dashboard", label:"Dashboard", emoji:"📊" },
{ id:"tracker", label:"Tracker", emoji:"🗺️" },
{ id:"leaderboard", label:"Rankings", emoji:"🏆" },
...(adminMode ? [{ id:"admin", label:"Admin", emoji:"🔧" }] : []),
];
return (
{/* NAV */}
setTab("dashboard")}>
VISIT THEM ALL
by Own All 50
{NAV_TABS.map(t => (
setTab(t.id)} style={{
background: "none", border: "none", cursor: "pointer",
padding: "18px 18px", fontFamily: "'DM Sans', sans-serif",
fontSize: 14, fontWeight: 600,
color: tab === t.id ? "#FF6B35" : "rgba(255,255,255,.55)",
borderBottom: tab === t.id ? "3px solid #FF6B35" : "3px solid transparent",
transition: "all .2s"
}}>{t.emoji} {t.label}
))}
{user}
{ adminMode ? setAdminMode(false) : setShowAdminLogin(true); }}
style={{
background: adminMode ? "rgba(255,107,53,.2)" : "rgba(255,255,255,.06)",
border: `1px solid ${adminMode ? "rgba(255,107,53,.4)" : "rgba(255,255,255,.12)"}`,
color: adminMode ? "#FF8C52" : "rgba(255,255,255,.4)",
borderRadius: 8, padding: "5px 12px", fontSize: 12, fontWeight: 700,
cursor: "pointer", fontFamily: "'DM Sans', sans-serif", transition: "all .2s"
}}>
🔧 {adminMode ? "Admin ON" : "Admin"}
{!isPaid && (
setShowPaywall(true)}>
Unlock Pro
)}
{/* VIEWS */}
{tab === "dashboard" && (
)}
{tab === "tracker" && (
setShowPaywall(true)}
mainCount={mainCount}
/>
)}
{tab === "leaderboard" && (
)}
{tab === "admin" && adminMode && (
)}
{/* MODALS */}
{showPaywall && (
setShowPaywall(false)}
onPay={() => { setIsPaid(true); setShowPaywall(false); }}
used={mainCount}
/>
)}
{showAdminLogin && (
setShowAdminLogin(false)}
onSuccess={() => { setAdminMode(true); setShowAdminLogin(false); setTab("admin"); }}
/>
)}
);
}