Upload frontend/src/App.tsx with huggingface_hub
Browse files- frontend/src/App.tsx +390 -0
frontend/src/App.tsx
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
| 2 |
+
import { Link, useNavigate } from "react-router-dom";
|
| 3 |
+
import ReactFlow, { Background, Controls, MiniMap, type Edge, type EdgeTypes, type Node, type NodeTypes } from "reactflow";
|
| 4 |
+
import "reactflow/dist/style.css";
|
| 5 |
+
import AnimatedPulseEdge from "./AnimatedPulseEdge";
|
| 6 |
+
import LakeCanvasNode, { type LakeCanvasNodeData } from "./components/LakeCanvasNode";
|
| 7 |
+
import Header from "./components/Header";
|
| 8 |
+
import Composer from "./components/Composer";
|
| 9 |
+
import AdminPanel from "./components/AdminPanel";
|
| 10 |
+
import PlayerTab from "./components/PlayerTab";
|
| 11 |
+
import LeadersTab from "./components/LeadersTab";
|
| 12 |
+
import QueriesTab, { type QueryResult } from "./components/QueriesTab";
|
| 13 |
+
import EngineTab from "./components/EngineTab";
|
| 14 |
+
import NewsTab from "./components/NewsTab";
|
| 15 |
+
import MatchupsTab from "./components/MatchupsTab";
|
| 16 |
+
import JimmyPanel from "./components/JimmyPanel";
|
| 17 |
+
import BooAgentPanel from "./components/BooAgentPanel";
|
| 18 |
+
import RampIndex from "./components/RampIndex";
|
| 19 |
+
import MyBooTab from "./components/MyBooTab";
|
| 20 |
+
import OddsTab from "./components/OddsTab";
|
| 21 |
+
import LiveTicker from "./components/LiveTicker";
|
| 22 |
+
import { api } from "./api";
|
| 23 |
+
import type { ChartIndexRow, PlayerPropChart } from "./types";
|
| 24 |
+
|
| 25 |
+
const VIEW_TABS = ["player", "leaders", "matchups", "ramp", "engine", "news", "queries", "myboo", "odds"] as const;
|
| 26 |
+
type ViewTab = (typeof VIEW_TABS)[number];
|
| 27 |
+
|
| 28 |
+
// These three are standalone pages — clicking them navigates away from the main app.
|
| 29 |
+
const STANDALONE_LINKS = [
|
| 30 |
+
{ id: "throwdown", label: "THROWDOWN THURSDAY", href: "/throwdown" },
|
| 31 |
+
{ id: "monday", label: "MONDAY NIGHT", href: "/monday" },
|
| 32 |
+
{ id: "sunday", label: "SUNDAY SLIPS", href: "/parlays" },
|
| 33 |
+
] as const;
|
| 34 |
+
|
| 35 |
+
const edgeTypes: EdgeTypes = { animatedPulse: AnimatedPulseEdge };
|
| 36 |
+
const nodeTypes: NodeTypes = { canvasNode: LakeCanvasNode };
|
| 37 |
+
|
| 38 |
+
export const tabPath = (t: string) => `/${t}`;
|
| 39 |
+
|
| 40 |
+
const DEFAULT_PLAYER_ID = "00-0033280"; // Christian McCaffrey -- a real player present in every gold CSV, sane default
|
| 41 |
+
|
| 42 |
+
export default function App({ initialTab }: { initialTab?: ViewTab }) {
|
| 43 |
+
const [isDark, setIsDark] = useState(true);
|
| 44 |
+
const [leftPct, setLeftPct] = useState(40);
|
| 45 |
+
const [rfNodes, setRfNodes] = useState<Node<LakeCanvasNodeData>[]>([]);
|
| 46 |
+
const [rfEdges, setRfEdges] = useState<Edge[]>([]);
|
| 47 |
+
const [nextNodeCount, setNextNodeCount] = useState(2);
|
| 48 |
+
|
| 49 |
+
const [tab, setTab] = useState<ViewTab>(() => {
|
| 50 |
+
if (initialTab) return initialTab;
|
| 51 |
+
try {
|
| 52 |
+
const t = new URLSearchParams(window.location.search).get("tab");
|
| 53 |
+
if (t && (VIEW_TABS as readonly string[]).includes(t)) return t as ViewTab;
|
| 54 |
+
} catch { /* */ }
|
| 55 |
+
return "leaders";
|
| 56 |
+
});
|
| 57 |
+
const navigate = useNavigate();
|
| 58 |
+
useEffect(() => { if (initialTab) setTab(initialTab); }, [initialTab]);
|
| 59 |
+
useEffect(() => {
|
| 60 |
+
const p = tabPath(tab);
|
| 61 |
+
if (window.location.pathname !== p) navigate(p, { replace: window.location.pathname === "/" });
|
| 62 |
+
}, [tab, navigate]);
|
| 63 |
+
const [canvasOpen, setCanvasOpen] = useState(false);
|
| 64 |
+
const [selectedPlayerId, setSelectedPlayerId] = useState(DEFAULT_PLAYER_ID);
|
| 65 |
+
const [playerMarket, setPlayerMarket] = useState<string | undefined>(undefined);
|
| 66 |
+
const [slip, setSlip] = useState<PlayerPropChart[]>([]);
|
| 67 |
+
const [target, setTarget] = useState("all");
|
| 68 |
+
const [adminOpen, setAdminOpen] = useState(false);
|
| 69 |
+
const [chartIndex, setChartIndex] = useState<ChartIndexRow[]>([]);
|
| 70 |
+
const [logs, setLogs] = useState<{ t: string; msg: string }[]>([{ t: now(), msg: "Canvas ready. Enter a query to begin." }]);
|
| 71 |
+
const [results, setResults] = useState<QueryResult[]>([]);
|
| 72 |
+
const [cleared, setCleared] = useState(false);
|
| 73 |
+
const [jimmyPanelOpen, setJimmyPanelOpen] = useState(false);
|
| 74 |
+
const [booPanelOpen, setBooPanelOpen] = useState(false);
|
| 75 |
+
|
| 76 |
+
useEffect(() => {
|
| 77 |
+
document.documentElement.setAttribute("data-theme", isDark ? "dark" : "light");
|
| 78 |
+
}, [isDark]);
|
| 79 |
+
|
| 80 |
+
useEffect(() => {
|
| 81 |
+
api.index().then(setChartIndex).catch(() => setChartIndex([]));
|
| 82 |
+
}, []);
|
| 83 |
+
|
| 84 |
+
// Data-driven nodes — fan-in topology: both RAMPs feed Jimmy at the bottom.
|
| 85 |
+
// NFL RAMP top-left, CFB RAMP top-right (offset right by ~Jimmy's node width
|
| 86 |
+
// to spread the converging edges), Jimmy centered below both.
|
| 87 |
+
useEffect(() => {
|
| 88 |
+
api.canvasNodes().then(({ nodes }) => {
|
| 89 |
+
const NODE_W = 210;
|
| 90 |
+
const positions: Record<string, { x: number; y: number }> = {
|
| 91 |
+
"ramp-nfl": { x: 40, y: 60 },
|
| 92 |
+
"ramp-cfb": { x: 40 + NODE_W + NODE_W + 30, y: 60 },
|
| 93 |
+
"jimmy-the-greek": { x: 40 + NODE_W + 15, y: 390 },
|
| 94 |
+
// the authority: MY BOO sits right beside Jimmy, same row
|
| 95 |
+
"my-boo": { x: 40 + NODE_W + 15 + NODE_W + 30, y: 390 },
|
| 96 |
+
};
|
| 97 |
+
|
| 98 |
+
// edges: every lake node feeds into every expert node
|
| 99 |
+
const lakeIds = nodes.filter((n) => n.type === "lake").map((n) => n.id);
|
| 100 |
+
const expertIds = nodes.filter((n) => n.type === "expert").map((n) => n.id);
|
| 101 |
+
const fanEdges: Edge[] = [];
|
| 102 |
+
for (const src of lakeIds) {
|
| 103 |
+
for (const tgt of expertIds) {
|
| 104 |
+
fanEdges.push({ id: `${src}->${tgt}`, source: src, target: tgt, type: "animatedPulse" });
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
const built = nodes.map((n, i) => ({
|
| 109 |
+
id: n.id,
|
| 110 |
+
type: "canvasNode",
|
| 111 |
+
position: positions[n.id] ?? { x: 40, y: 40 + i * 340 },
|
| 112 |
+
data: {
|
| 113 |
+
type: n.type, name: n.name, sub: n.sub, chips: n.chips,
|
| 114 |
+
...(n.store ? { store: n.store } : {}),
|
| 115 |
+
...(n.type === "expert" ? { onSettings: () => (n.id === "my-boo" ? setBooPanelOpen(true) : setJimmyPanelOpen(true)) } : {}),
|
| 116 |
+
},
|
| 117 |
+
}));
|
| 118 |
+
|
| 119 |
+
setRfNodes(built);
|
| 120 |
+
setRfEdges(fanEdges);
|
| 121 |
+
setNextNodeCount(built.length);
|
| 122 |
+
});
|
| 123 |
+
}, []);
|
| 124 |
+
|
| 125 |
+
function log(msg: string) {
|
| 126 |
+
setLogs((l) => [...l, { t: now(), msg }]);
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
function addNode(type: "lake" | "expert") {
|
| 130 |
+
const id = `${type}-${Date.now()}`;
|
| 131 |
+
const newNode: Node<LakeCanvasNodeData> = {
|
| 132 |
+
id,
|
| 133 |
+
type: "canvasNode",
|
| 134 |
+
position: { x: 40, y: nextNodeCount * 340 + 16 },
|
| 135 |
+
data: type === "lake"
|
| 136 |
+
? { type, name: "NEW RAMP SOURCE", sub: "Phase 1 — the lake" }
|
| 137 |
+
: { type, name: "NEW EXPERT", sub: "harness · not wired", chips: [] },
|
| 138 |
+
};
|
| 139 |
+
setRfNodes((prev) => {
|
| 140 |
+
const updated = [...prev, newNode];
|
| 141 |
+
if (prev.length > 0) {
|
| 142 |
+
setRfEdges((edges) => [...edges, { id: `${prev[prev.length - 1].id}-${id}`, source: prev[prev.length - 1].id, target: id, type: "animatedPulse" }]);
|
| 143 |
+
}
|
| 144 |
+
return updated;
|
| 145 |
+
});
|
| 146 |
+
setNextNodeCount((n) => n + 1);
|
| 147 |
+
log(type === "lake" ? "RAMP source node added" : "Expert node added to harness");
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const slipIds = useMemo(() => new Set(slip.map((s) => s.playerId)), [slip]);
|
| 151 |
+
|
| 152 |
+
function addToSlip(chart: PlayerPropChart) {
|
| 153 |
+
setSlip((prev) => (prev.some((s) => s.playerId === chart.playerId) ? prev : [...prev, chart]));
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
const targets = useMemo(
|
| 159 |
+
() => [{ id: "all", label: "All nodes" }, ...rfNodes.map((n) => ({ id: n.id, label: n.data.name }))],
|
| 160 |
+
[rfNodes]
|
| 161 |
+
);
|
| 162 |
+
|
| 163 |
+
const handleSubmit = useCallback(
|
| 164 |
+
(text: string) => {
|
| 165 |
+
setCleared(false);
|
| 166 |
+
const n = results.length + 1;
|
| 167 |
+
setResults((prev) => [
|
| 168 |
+
{
|
| 169 |
+
n,
|
| 170 |
+
time: now(),
|
| 171 |
+
title: text,
|
| 172 |
+
subtitle: `Targeted: ${targets.find((t) => t.id === target)?.label ?? "All nodes"} — echoed, no live query routing wired yet`,
|
| 173 |
+
stats: [
|
| 174 |
+
{ label: "Target", value: targets.find((t) => t.id === target)?.label ?? "All nodes" },
|
| 175 |
+
{ label: "Result", value: "not wired — see RAMP NFL / JIMMY THE GREEK nodes" },
|
| 176 |
+
],
|
| 177 |
+
},
|
| 178 |
+
...prev,
|
| 179 |
+
]);
|
| 180 |
+
setTab("queries");
|
| 181 |
+
log(`Query submitted: "${text}"`);
|
| 182 |
+
},
|
| 183 |
+
[results, target, targets]
|
| 184 |
+
);
|
| 185 |
+
|
| 186 |
+
function handleClear() {
|
| 187 |
+
setResults([]);
|
| 188 |
+
setCleared(true);
|
| 189 |
+
log("Display cleared");
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
const canvasCapable = tab !== "leaders" && tab !== "matchups" && tab !== "ramp" && tab !== "myboo" && tab !== "odds";
|
| 193 |
+
|
| 194 |
+
return (
|
| 195 |
+
<div style={{ display: "flex", flexDirection: "column", width: "100vw", height: "100vh", background: "var(--bp-page-bg)", color: "var(--bp-fg)" }}>
|
| 196 |
+
<Header isDark={isDark} onToggleTheme={() => setIsDark((v) => !v)} onToggleAdmin={() => setAdminOpen((v) => !v)} />
|
| 197 |
+
<LiveTicker />
|
| 198 |
+
|
| 199 |
+
<div style={{ flex: 1, display: "flex", minHeight: 0, position: "relative" }}>
|
| 200 |
+
{canvasCapable && !canvasOpen && (
|
| 201 |
+
<button onClick={() => setCanvasOpen(true)} title="Show the node canvas"
|
| 202 |
+
style={{ position: "absolute", left: 0, top: "40%", zIndex: 20, writingMode: "vertical-rl", padding: "14px 6px", cursor: "pointer",
|
| 203 |
+
border: "1px solid #6b4a1c", borderLeft: 0, borderRadius: "0 10px 10px 0", background: "var(--bp-card-bg)", color: "#d9b45a",
|
| 204 |
+
fontSize: 11, fontWeight: 800, letterSpacing: "0.2em" }}>
|
| 205 |
+
NODES ▸
|
| 206 |
+
</button>
|
| 207 |
+
)}
|
| 208 |
+
{canvasOpen && canvasCapable && (
|
| 209 |
+
<>
|
| 210 |
+
<div style={{ width: `${leftPct}%`, flex: `0 0 ${leftPct}%`, display: "flex", flexDirection: "column", minWidth: 0 }}>
|
| 211 |
+
<div
|
| 212 |
+
style={{
|
| 213 |
+
flex: 1,
|
| 214 |
+
position: "relative",
|
| 215 |
+
overflow: "hidden",
|
| 216 |
+
background:
|
| 217 |
+
"radial-gradient(ellipse 70% 55% at 28% 30%, var(--bp-canvas-glow-orange), transparent 70%), radial-gradient(ellipse 80% 60% at 70% 78%, var(--bp-canvas-glow-purple), transparent 70%), var(--bp-canvas-bg)",
|
| 218 |
+
}}
|
| 219 |
+
>
|
| 220 |
+
<button onClick={() => setCanvasOpen(false)} title="Hide the node canvas"
|
| 221 |
+
style={{ position: "absolute", left: 10, top: 10, zIndex: 20, height: 28, padding: "0 12px", cursor: "pointer", borderRadius: 999,
|
| 222 |
+
border: "1px solid #6b4a1c", background: "var(--bp-card-bg)", color: "#d9b45a", fontSize: 11, fontWeight: 800, letterSpacing: "0.12em" }}>
|
| 223 |
+
◂ HIDE NODES
|
| 224 |
+
</button>
|
| 225 |
+
<ReactFlow nodes={rfNodes} edges={rfEdges} nodeTypes={nodeTypes} edgeTypes={edgeTypes} fitView minZoom={0.2} maxZoom={1.2}>
|
| 226 |
+
<Background color="#2a1e36" gap={24} />
|
| 227 |
+
<Controls position="bottom-right" style={{ marginBottom: 100 }} />
|
| 228 |
+
<MiniMap
|
| 229 |
+
position="bottom-right"
|
| 230 |
+
style={{ width: 112, height: 72, background: "#140a20", border: "1px solid #2a1e36", borderRadius: 4 }}
|
| 231 |
+
maskColor="rgba(11,5,18,0.6)"
|
| 232 |
+
nodeColor="#6a4a86"
|
| 233 |
+
pannable={false}
|
| 234 |
+
zoomable={false}
|
| 235 |
+
/>
|
| 236 |
+
</ReactFlow>
|
| 237 |
+
</div>
|
| 238 |
+
</div>
|
| 239 |
+
|
| 240 |
+
<ResizeHandle onChange={setLeftPct} />
|
| 241 |
+
</>
|
| 242 |
+
)}
|
| 243 |
+
|
| 244 |
+
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", background: "var(--bp-page-bg)", color: "var(--bp-fg)", position: "relative" }}>
|
| 245 |
+
<div style={{ height: 8, flex: "0 0 8px" }} />
|
| 246 |
+
|
| 247 |
+
<div style={{ display: "flex", gap: 6, padding: "0 20px 10px", flexWrap: "wrap", flex: "0 0 auto" }}>
|
| 248 |
+
{/* HOME — landing page */}
|
| 249 |
+
<a
|
| 250 |
+
href="/home/index.html"
|
| 251 |
+
style={{
|
| 252 |
+
display: "inline-flex", alignItems: "center",
|
| 253 |
+
height: 32, padding: "0 14px", borderRadius: 999,
|
| 254 |
+
border: "1px solid #c9a54e",
|
| 255 |
+
background: "rgba(201,165,78,0.14)",
|
| 256 |
+
color: "#f1dc92",
|
| 257 |
+
fontSize: 12, fontWeight: 800, letterSpacing: "0.1em",
|
| 258 |
+
textDecoration: "none", cursor: "pointer",
|
| 259 |
+
}}
|
| 260 |
+
>
|
| 261 |
+
HOME
|
| 262 |
+
</a>
|
| 263 |
+
{/* Standalone page links — navigate away */}
|
| 264 |
+
{STANDALONE_LINKS.map(({ id, label, href }) => (
|
| 265 |
+
<Link
|
| 266 |
+
key={id}
|
| 267 |
+
to={href}
|
| 268 |
+
style={{
|
| 269 |
+
display: "inline-flex", alignItems: "center",
|
| 270 |
+
height: 32, padding: "0 14px", borderRadius: 999,
|
| 271 |
+
border: "1px solid #c9a54e",
|
| 272 |
+
background: "rgba(201,165,78,0.14)",
|
| 273 |
+
color: "#f1dc92",
|
| 274 |
+
fontSize: 12, fontWeight: 800, letterSpacing: "0.1em",
|
| 275 |
+
textDecoration: "none", cursor: "pointer",
|
| 276 |
+
}}
|
| 277 |
+
>
|
| 278 |
+
{label}
|
| 279 |
+
</Link>
|
| 280 |
+
))}
|
| 281 |
+
{/* In-app tabs */}
|
| 282 |
+
{VIEW_TABS.filter((vt) => vt !== "player").map((vt) => {
|
| 283 |
+
const isBoo = vt === "myboo";
|
| 284 |
+
const isActive = vt === tab;
|
| 285 |
+
return (
|
| 286 |
+
<button
|
| 287 |
+
key={vt}
|
| 288 |
+
onClick={() => setTab(vt)}
|
| 289 |
+
style={{
|
| 290 |
+
height: 32, padding: "0 14px", borderRadius: 999,
|
| 291 |
+
border: `1px solid ${isActive ? (isBoo ? "#a78bfa" : "#c9a54e") : (isBoo ? "#3a2060" : "var(--bp-border)")}`,
|
| 292 |
+
background: isActive
|
| 293 |
+
? (isBoo ? "rgba(139,92,246,0.18)" : "rgba(201,165,78,0.14)")
|
| 294 |
+
: (isBoo ? "rgba(139,92,246,0.06)" : "var(--bp-card-bg)"),
|
| 295 |
+
color: isActive ? (isBoo ? "#c4b5fd" : "#d9b45a") : (isBoo ? "#a78bfa" : "var(--bp-fg)"),
|
| 296 |
+
fontSize: 12, fontWeight: isBoo ? 900 : 700, cursor: "pointer",
|
| 297 |
+
letterSpacing: isBoo ? "0.06em" : undefined,
|
| 298 |
+
}}
|
| 299 |
+
>
|
| 300 |
+
{isBoo ? "MY BOO" : vt === "odds" ? "THE ODDS" : vt.toUpperCase()}
|
| 301 |
+
</button>
|
| 302 |
+
);
|
| 303 |
+
})}
|
| 304 |
+
</div>
|
| 305 |
+
|
| 306 |
+
<div style={{ flex: 1, overflowY: "auto", padding: "4px 20px 20px", minHeight: 0 }}>
|
| 307 |
+
{tab === "ramp" && <RampIndex />}
|
| 308 |
+
{tab === "player" && (
|
| 309 |
+
<PlayerTab
|
| 310 |
+
playerId={selectedPlayerId}
|
| 311 |
+
initialMarket={playerMarket}
|
| 312 |
+
onSelectPlayer={(id) => { setPlayerMarket(undefined); setSelectedPlayerId(id); }}
|
| 313 |
+
onSlipAdd={addToSlip}
|
| 314 |
+
inSlip={slipIds.has(selectedPlayerId)}
|
| 315 |
+
/>
|
| 316 |
+
)}
|
| 317 |
+
{tab === "leaders" && (
|
| 318 |
+
<LeadersTab
|
| 319 |
+
onOpenPlayer={(id, market) => {
|
| 320 |
+
setPlayerMarket(market);
|
| 321 |
+
setSelectedPlayerId(id);
|
| 322 |
+
setTab("player");
|
| 323 |
+
}}
|
| 324 |
+
/>
|
| 325 |
+
)}
|
| 326 |
+
{/* parlay tab removed — THROWDOWN THURSDAY is now a standalone page at /throwdown */}
|
| 327 |
+
{tab === "engine" && <EngineTab />}
|
| 328 |
+
{tab === "news" && <NewsTab />}
|
| 329 |
+
{tab === "matchups" && <MatchupsTab />}
|
| 330 |
+
{tab === "queries" && (cleared && results.length === 0 ? <QueriesTab results={[]} /> : <QueriesTab results={results} />)}
|
| 331 |
+
{tab === "myboo" && <MyBooTab />}
|
| 332 |
+
{tab === "odds" && <OddsTab />}
|
| 333 |
+
</div>
|
| 334 |
+
|
| 335 |
+
<Composer
|
| 336 |
+
targets={targets}
|
| 337 |
+
target={target}
|
| 338 |
+
onTargetChange={setTarget}
|
| 339 |
+
onSubmit={handleSubmit}
|
| 340 |
+
onClear={handleClear}
|
| 341 |
+
counter={results.length}
|
| 342 |
+
/>
|
| 343 |
+
|
| 344 |
+
{adminOpen && (
|
| 345 |
+
<AdminPanel
|
| 346 |
+
onClose={() => setAdminOpen(false)}
|
| 347 |
+
onAddRamp={() => addNode("lake")}
|
| 348 |
+
onAddExpert={() => addNode("expert")}
|
| 349 |
+
chartIndex={chartIndex}
|
| 350 |
+
logs={logs}
|
| 351 |
+
/>
|
| 352 |
+
)}
|
| 353 |
+
|
| 354 |
+
{jimmyPanelOpen && (
|
| 355 |
+
<JimmyPanel onClose={() => setJimmyPanelOpen(false)} />
|
| 356 |
+
)}
|
| 357 |
+
{booPanelOpen && <BooAgentPanel onClose={() => setBooPanelOpen(false)} />}
|
| 358 |
+
</div>
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
function ResizeHandle({ onChange }: { onChange: (pct: number) => void }) {
|
| 365 |
+
function onPointerDown(e: React.PointerEvent<HTMLDivElement>) {
|
| 366 |
+
e.preventDefault();
|
| 367 |
+
const root = (e.currentTarget.parentElement as HTMLElement) ?? document.body;
|
| 368 |
+
function move(ev: PointerEvent) {
|
| 369 |
+
const rect = root.getBoundingClientRect();
|
| 370 |
+
const pct = Math.max(22, Math.min(65, ((ev.clientX - rect.left) / rect.width) * 100));
|
| 371 |
+
onChange(pct);
|
| 372 |
+
}
|
| 373 |
+
function up() {
|
| 374 |
+
window.removeEventListener("pointermove", move);
|
| 375 |
+
window.removeEventListener("pointerup", up);
|
| 376 |
+
}
|
| 377 |
+
window.addEventListener("pointermove", move);
|
| 378 |
+
window.addEventListener("pointerup", up);
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
+
return (
|
| 382 |
+
<div onPointerDown={onPointerDown} title="Drag to resize" style={{ width: 6, flex: "0 0 6px", cursor: "col-resize", background: "#241e2c", position: "relative", zIndex: 5 }}>
|
| 383 |
+
<div style={{ position: "absolute", left: "50%", top: "50%", transform: "translate(-50%,-50%)", width: 3, height: 44, borderRadius: 3, background: "#4a4058" }} />
|
| 384 |
+
</div>
|
| 385 |
+
);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
function now(): string {
|
| 389 |
+
return new Date().toTimeString().slice(0, 5);
|
| 390 |
+
}
|