Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>NBA Dashboard</title> | |
| <!-- CodeMirror --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.css"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/theme/dracula.min.css"> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/codemirror.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.13/mode/python/python.min.js"></script> | |
| <!-- Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <!-- Font Awesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --bg-primary: #0a0a0f; | |
| --bg-secondary: #12121a; | |
| --bg-card: #1a1a25; | |
| --bg-card-hover: #222230; | |
| --accent-orange: #f97316; | |
| --accent-blue: #3b82f6; | |
| --accent-purple: #8b5cf6; | |
| --accent-green: #22c55e; | |
| --accent-red: #ef4444; | |
| --accent-yellow: #eab308; | |
| --text-primary: #ffffff; | |
| --text-secondary: #a1a1aa; | |
| --text-muted: #71717a; | |
| --border: rgba(255,255,255,0.08); | |
| --border-light: rgba(255,255,255,0.12); | |
| --glow-orange: rgba(249, 115, 22, 0.3); | |
| --glow-blue: rgba(59, 130, 246, 0.3); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| background: var(--bg-primary); | |
| color: var(--text-primary); | |
| font-family: 'Inter', -apple-system, sans-serif; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Background Effects */ | |
| .bg-effects { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| pointer-events: none; | |
| z-index: 0; | |
| overflow: hidden; | |
| } | |
| .bg-orb { | |
| position: absolute; | |
| border-radius: 50%; | |
| filter: blur(100px); | |
| opacity: 0.4; | |
| animation: float 20s ease-in-out infinite; | |
| } | |
| .bg-orb-1 { | |
| width: 600px; | |
| height: 600px; | |
| background: linear-gradient(135deg, var(--accent-orange), var(--accent-red)); | |
| top: -200px; | |
| right: -100px; | |
| } | |
| .bg-orb-2 { | |
| width: 500px; | |
| height: 500px; | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| bottom: -150px; | |
| left: -100px; | |
| animation-delay: -10s; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translate(0, 0) scale(1); } | |
| 50% { transform: translate(30px, -30px) scale(1.1); } | |
| } | |
| /* Navbar */ | |
| nav { | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| background: rgba(10, 10, 15, 0.8); | |
| backdrop-filter: blur(20px); | |
| border-bottom: 1px solid var(--border); | |
| padding: 0 2rem; | |
| } | |
| .nav-container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| height: 70px; | |
| } | |
| .brand { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| text-decoration: none; | |
| } | |
| .brand-logo { | |
| width: 45px; | |
| height: 45px; | |
| background: linear-gradient(135deg, var(--accent-orange), var(--accent-red)); | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.4rem; | |
| box-shadow: 0 4px 20px var(--glow-orange); | |
| } | |
| .brand-text { | |
| font-size: 1.5rem; | |
| font-weight: 800; | |
| letter-spacing: -0.5px; | |
| } | |
| .brand-text span { | |
| background: linear-gradient(135deg, var(--accent-orange), var(--accent-red)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| .nav-actions { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .nav-btn { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| color: var(--text-secondary); | |
| padding: 10px 16px; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-size: 0.9rem; | |
| font-weight: 500; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| transition: all 0.2s ease; | |
| } | |
| .nav-btn:hover { | |
| background: var(--bg-card-hover); | |
| color: var(--text-primary); | |
| border-color: var(--border-light); | |
| transform: translateY(-1px); | |
| } | |
| .nav-btn.primary { | |
| background: linear-gradient(135deg, var(--accent-orange), var(--accent-red)); | |
| border: none; | |
| color: white; | |
| box-shadow: 0 4px 15px var(--glow-orange); | |
| } | |
| .nav-btn.primary:hover { | |
| box-shadow: 0 6px 25px var(--glow-orange); | |
| } | |
| /* Main Content */ | |
| .main-content { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 2rem; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| /* Section Tabs */ | |
| .section-tabs { | |
| display: flex; | |
| gap: 8px; | |
| margin-bottom: 2rem; | |
| padding: 6px; | |
| background: var(--bg-secondary); | |
| border-radius: 16px; | |
| width: fit-content; | |
| border: 1px solid var(--border); | |
| } | |
| .tab-btn { | |
| padding: 12px 24px; | |
| border: none; | |
| background: transparent; | |
| color: var(--text-secondary); | |
| font-size: 0.95rem; | |
| font-weight: 600; | |
| border-radius: 12px; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .tab-btn:hover { | |
| color: var(--text-primary); | |
| background: rgba(255,255,255,0.05); | |
| } | |
| .tab-btn.active { | |
| background: var(--bg-card); | |
| color: var(--text-primary); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.3); | |
| } | |
| .tab-icon { | |
| font-size: 1rem; | |
| } | |
| /* Section Header */ | |
| .section-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 1.5rem; | |
| } | |
| .section-title { | |
| display: flex; | |
| align-items: center; | |
| gap: 16px; | |
| } | |
| .section-title h2 { | |
| font-size: 1.8rem; | |
| font-weight: 700; | |
| letter-spacing: -0.5px; | |
| } | |
| .live-badge { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| padding: 6px 14px; | |
| background: rgba(239, 68, 68, 0.15); | |
| border: 1px solid rgba(239, 68, 68, 0.3); | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| color: var(--accent-red); | |
| } | |
| .live-dot { | |
| width: 8px; | |
| height: 8px; | |
| background: var(--accent-red); | |
| border-radius: 50%; | |
| animation: pulse 1.5s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 1; transform: scale(1); } | |
| 50% { opacity: 0.5; transform: scale(1.2); } | |
| } | |
| .date-display { | |
| color: var(--text-muted); | |
| font-size: 0.95rem; | |
| font-weight: 500; | |
| } | |
| /* Games Grid */ | |
| .games-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); | |
| gap: 1.25rem; | |
| } | |
| /* Game Card */ | |
| .game-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| padding: 1.5rem; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .game-card::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, var(--accent-orange), var(--accent-blue)); | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .game-card:hover { | |
| border-color: var(--border-light); | |
| transform: translateY(-4px); | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.4); | |
| } | |
| .game-card:hover::before { | |
| opacity: 1; | |
| } | |
| .game-card.live { | |
| border-color: rgba(239, 68, 68, 0.3); | |
| } | |
| .game-card.live::before { | |
| background: var(--accent-red); | |
| opacity: 1; | |
| } | |
| .game-status { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 1.25rem; | |
| } | |
| .status-badge { | |
| padding: 5px 12px; | |
| border-radius: 8px; | |
| font-size: 0.75rem; | |
| font-weight: 700; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .status-badge.live { | |
| background: rgba(239, 68, 68, 0.15); | |
| color: var(--accent-red); | |
| animation: status-pulse 2s ease infinite; | |
| } | |
| @keyframes status-pulse { | |
| 0%, 100% { background: rgba(239, 68, 68, 0.15); } | |
| 50% { background: rgba(239, 68, 68, 0.25); } | |
| } | |
| .status-badge.upcoming { | |
| background: rgba(59, 130, 246, 0.15); | |
| color: var(--accent-blue); | |
| } | |
| .status-badge.final { | |
| background: rgba(113, 113, 122, 0.15); | |
| color: var(--text-secondary); | |
| } | |
| .game-time { | |
| color: var(--text-muted); | |
| font-size: 0.85rem; | |
| font-weight: 500; | |
| } | |
| .teams-container { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 1rem; | |
| } | |
| .team-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 0.75rem; | |
| background: rgba(255,255,255,0.02); | |
| border-radius: 12px; | |
| transition: background 0.2s ease; | |
| } | |
| .team-row:hover { | |
| background: rgba(255,255,255,0.05); | |
| } | |
| .team-row.winner { | |
| background: rgba(34, 197, 94, 0.08); | |
| } | |
| .team-info { | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| } | |
| .team-logo { | |
| width: 48px; | |
| height: 48px; | |
| border-radius: 12px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: 800; | |
| font-size: 0.8rem; | |
| color: white; | |
| text-shadow: 0 1px 2px rgba(0,0,0,0.3); | |
| } | |
| .team-details { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| } | |
| .team-name { | |
| font-weight: 700; | |
| font-size: 1rem; | |
| color: var(--text-primary); | |
| } | |
| .team-record { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| .team-score { | |
| font-size: 1.75rem; | |
| font-weight: 800; | |
| color: var(--text-primary); | |
| min-width: 60px; | |
| text-align: right; | |
| } | |
| .team-score.leading { | |
| color: var(--accent-green); | |
| } | |
| .game-footer { | |
| margin-top: 1.25rem; | |
| padding-top: 1rem; | |
| border-top: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .game-channel { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 0.85rem; | |
| color: var(--text-muted); | |
| } | |
| .game-channel i { | |
| color: var(--accent-blue); | |
| } | |
| .game-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .action-btn { | |
| padding: 8px 12px; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| color: var(--text-secondary); | |
| font-size: 0.8rem; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .action-btn:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: var(--text-primary); | |
| } | |
| /* Loading State */ | |
| .loading-container { | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 4rem; | |
| gap: 1.5rem; | |
| } | |
| .loading-spinner { | |
| width: 50px; | |
| height: 50px; | |
| border: 3px solid var(--border); | |
| border-top-color: var(--accent-orange); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| .loading-text { | |
| color: var(--text-secondary); | |
| font-size: 0.95rem; | |
| } | |
| /* Empty State */ | |
| .empty-state { | |
| display: none; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 4rem; | |
| text-align: center; | |
| } | |
| .empty-icon { | |
| font-size: 4rem; | |
| margin-bottom: 1.5rem; | |
| opacity: 0.5; | |
| } | |
| .empty-title { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| } | |
| .empty-desc { | |
| color: var(--text-muted); | |
| max-width: 300px; | |
| } | |
| /* Raw Output (fallback) */ | |
| .raw-output { | |
| display: none; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| padding: 1.5rem; | |
| font-family: 'JetBrains Mono', monospace; | |
| font-size: 0.9rem; | |
| line-height: 1.7; | |
| white-space: pre-wrap; | |
| color: var(--text-secondary); | |
| max-height: 600px; | |
| overflow-y: auto; | |
| } | |
| .raw-output.error { | |
| border-color: rgba(239, 68, 68, 0.3); | |
| color: var(--accent-red); | |
| } | |
| /* Settings Modal */ | |
| .modal-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(10px); | |
| display: none; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 1000; | |
| } | |
| .modal { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 24px; | |
| padding: 2rem; | |
| width: 90%; | |
| max-width: 420px; | |
| animation: modalIn 0.3s ease; | |
| } | |
| @keyframes modalIn { | |
| from { opacity: 0; transform: scale(0.95) translateY(20px); } | |
| to { opacity: 1; transform: scale(1) translateY(0); } | |
| } | |
| .modal-header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 1.5rem; | |
| } | |
| .modal-title { | |
| font-size: 1.25rem; | |
| font-weight: 700; | |
| } | |
| .modal-close { | |
| background: none; | |
| border: none; | |
| color: var(--text-muted); | |
| font-size: 1.25rem; | |
| cursor: pointer; | |
| padding: 8px; | |
| border-radius: 8px; | |
| transition: all 0.2s; | |
| } | |
| .modal-close:hover { | |
| background: rgba(255,255,255,0.1); | |
| color: var(--text-primary); | |
| } | |
| .form-group { | |
| margin-bottom: 1.5rem; | |
| } | |
| .form-label { | |
| display: block; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| margin-bottom: 0.5rem; | |
| color: var(--text-secondary); | |
| } | |
| .form-input { | |
| width: 100%; | |
| padding: 14px 16px; | |
| background: var(--bg-secondary); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| color: var(--text-primary); | |
| font-size: 0.95rem; | |
| transition: all 0.2s; | |
| } | |
| .form-input:focus { | |
| outline: none; | |
| border-color: var(--accent-orange); | |
| box-shadow: 0 0 0 3px var(--glow-orange); | |
| } | |
| .form-input::placeholder { | |
| color: var(--text-muted); | |
| } | |
| .modal-btn { | |
| width: 100%; | |
| padding: 14px; | |
| background: linear-gradient(135deg, var(--accent-orange), var(--accent-red)); | |
| border: none; | |
| border-radius: 12px; | |
| color: white; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .modal-btn:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 25px var(--glow-orange); | |
| } | |
| /* Dev Panel */ | |
| .dev-panel { | |
| position: fixed; | |
| top: 0; | |
| right: 0; | |
| bottom: 0; | |
| width: 450px; | |
| background: var(--bg-secondary); | |
| border-left: 1px solid var(--border); | |
| transform: translateX(100%); | |
| transition: transform 0.3s ease; | |
| z-index: 200; | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .dev-panel.open { | |
| transform: translateX(0); | |
| } | |
| .dev-header { | |
| padding: 1rem 1.5rem; | |
| background: var(--bg-card); | |
| border-bottom: 1px solid var(--border); | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| } | |
| .dev-title { | |
| font-weight: 700; | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .dev-title i { | |
| color: var(--accent-purple); | |
| } | |
| .dev-actions { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .dev-btn { | |
| padding: 8px 16px; | |
| border-radius: 8px; | |
| border: none; | |
| font-size: 0.85rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| } | |
| .dev-btn.run { | |
| background: var(--accent-green); | |
| color: white; | |
| } | |
| .dev-btn.run:hover { | |
| background: #16a34a; | |
| } | |
| .dev-btn.close { | |
| background: rgba(255,255,255,0.1); | |
| color: var(--text-secondary); | |
| } | |
| .CodeMirror { | |
| flex: 1; | |
| font-size: 13px; | |
| font-family: 'JetBrains Mono', monospace; | |
| } | |
| /* Sidebar Scripts */ | |
| .scripts-section { | |
| margin-top: 2rem; | |
| padding-top: 2rem; | |
| border-top: 1px solid var(--border); | |
| } | |
| .scripts-title { | |
| font-size: 1rem; | |
| font-weight: 600; | |
| color: var(--text-secondary); | |
| margin-bottom: 1rem; | |
| } | |
| .scripts-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); | |
| gap: 1rem; | |
| } | |
| .script-card { | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 14px; | |
| padding: 1.25rem; | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .script-card:hover { | |
| border-color: var(--border-light); | |
| transform: translateY(-2px); | |
| box-shadow: 0 10px 30px rgba(0,0,0,0.3); | |
| } | |
| .script-icon { | |
| width: 42px; | |
| height: 42px; | |
| border-radius: 10px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-size: 1.25rem; | |
| margin-bottom: 1rem; | |
| } | |
| .script-icon.orange { background: rgba(249, 115, 22, 0.15); color: var(--accent-orange); } | |
| .script-icon.blue { background: rgba(59, 130, 246, 0.15); color: var(--accent-blue); } | |
| .script-icon.purple { background: rgba(139, 92, 246, 0.15); color: var(--accent-purple); } | |
| .script-icon.green { background: rgba(34, 197, 94, 0.15); color: var(--accent-green); } | |
| .script-name { | |
| font-weight: 600; | |
| margin-bottom: 0.25rem; | |
| } | |
| .script-desc { | |
| font-size: 0.8rem; | |
| color: var(--text-muted); | |
| } | |
| /* Team Colors */ | |
| .team-ATL { background: linear-gradient(135deg, #E03A3E, #C1D32F); } | |
| .team-BOS { background: linear-gradient(135deg, #007A33, #BA9653); } | |
| .team-BKN { background: linear-gradient(135deg, #000000, #FFFFFF); } | |
| .team-CHA { background: linear-gradient(135deg, #1D1160, #00788C); } | |
| .team-CHI { background: linear-gradient(135deg, #CE1141, #000000); } | |
| .team-CLE { background: linear-gradient(135deg, #860038, #FDBB30); } | |
| .team-DAL { background: linear-gradient(135deg, #00538C, #002B5E); } | |
| .team-DEN { background: linear-gradient(135deg, #0E2240, #FEC524); } | |
| .team-DET { background: linear-gradient(135deg, #C8102E, #1D42BA); } | |
| .team-GSW { background: linear-gradient(135deg, #1D428A, #FFC72C); } | |
| .team-HOU { background: linear-gradient(135deg, #CE1141, #000000); } | |
| .team-IND { background: linear-gradient(135deg, #002D62, #FDBB30); } | |
| .team-LAC { background: linear-gradient(135deg, #C8102E, #1D428A); } | |
| .team-LAL { background: linear-gradient(135deg, #552583, #FDB927); } | |
| .team-MEM { background: linear-gradient(135deg, #5D76A9, #12173F); } | |
| .team-MIA { background: linear-gradient(135deg, #98002E, #F9A01B); } | |
| .team-MIL { background: linear-gradient(135deg, #00471B, #EEE1C6); } | |
| .team-MIN { background: linear-gradient(135deg, #0C2340, #236192); } | |
| .team-NOP { background: linear-gradient(135deg, #0C2340, #C8102E); } | |
| .team-NYK { background: linear-gradient(135deg, #006BB6, #F58426); } | |
| .team-OKC { background: linear-gradient(135deg, #007AC1, #EF3B24); } | |
| .team-ORL { background: linear-gradient(135deg, #0077C0, #000000); } | |
| .team-PHI { background: linear-gradient(135deg, #006BB6, #ED174C); } | |
| .team-PHX { background: linear-gradient(135deg, #1D1160, #E56020); } | |
| .team-POR { background: linear-gradient(135deg, #E03A3E, #000000); } | |
| .team-SAC { background: linear-gradient(135deg, #5A2D81, #63727A); } | |
| .team-SAS { background: linear-gradient(135deg, #C4CED4, #000000); } | |
| .team-TOR { background: linear-gradient(135deg, #CE1141, #000000); } | |
| .team-UTA { background: linear-gradient(135deg, #002B5C, #00471B); } | |
| .team-WAS { background: linear-gradient(135deg, #002B5C, #E31837); } | |
| .team-DEFAULT { background: linear-gradient(135deg, #333, #666); } | |
| /* Responsive */ | |
| @media (max-width: 768px) { | |
| .nav-container { padding: 0 1rem; } | |
| .main-content { padding: 1rem; } | |
| .games-grid { grid-template-columns: 1fr; } | |
| .section-tabs { flex-wrap: wrap; width: 100%; } | |
| .tab-btn { flex: 1; justify-content: center; } | |
| .dev-panel { width: 100%; } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Background Effects --> | |
| <div class="bg-effects"> | |
| <div class="bg-orb bg-orb-1"></div> | |
| <div class="bg-orb bg-orb-2"></div> | |
| </div> | |
| <!-- Navigation --> | |
| <nav> | |
| <div class="nav-container"> | |
| <a href="#" class="brand"> | |
| <div class="brand-logo">🏀</div> | |
| <div class="brand-text"><span>NBA</span>Hub</div> | |
| </a> | |
| <div class="nav-actions"> | |
| <button class="nav-btn" onclick="toggleDev()" title="Code Editor"> | |
| <i class="fas fa-code"></i> | |
| <span>Dev</span> | |
| </button> | |
| <button class="nav-btn" onclick="toggleSettings()" title="Settings"> | |
| <i class="fas fa-cog"></i> | |
| </button> | |
| <button class="nav-btn primary" onclick="runScript('nba_schedule.py')" title="Refresh"> | |
| <i class="fas fa-sync-alt"></i> | |
| <span>Refresh</span> | |
| </button> | |
| </div> | |
| </div> | |
| </nav> | |
| <!-- Main Content --> | |
| <div class="main-content"> | |
| <!-- Section Tabs --> | |
| <div class="section-tabs"> | |
| <button class="tab-btn active" onclick="switchTab('games')"> | |
| <i class="fas fa-basketball-ball tab-icon"></i> | |
| Games | |
| </button> | |
| <button class="tab-btn" onclick="switchTab('standings')"> | |
| <i class="fas fa-trophy tab-icon"></i> | |
| Standings | |
| </button> | |
| <button class="tab-btn" onclick="switchTab('stats')"> | |
| <i class="fas fa-chart-bar tab-icon"></i> | |
| Stats | |
| </button> | |
| <button class="tab-btn" onclick="switchTab('players')"> | |
| <i class="fas fa-user tab-icon"></i> | |
| Players | |
| </button> | |
| </div> | |
| <!-- Section Header --> | |
| <div class="section-header"> | |
| <div class="section-title"> | |
| <h2>Today's Games</h2> | |
| <div class="live-badge" id="live-badge" style="display: none;"> | |
| <div class="live-dot"></div> | |
| <span>LIVE</span> | |
| </div> | |
| </div> | |
| <div class="date-display" id="current-date"></div> | |
| </div> | |
| <!-- Loading State --> | |
| <div class="loading-container" id="loader"> | |
| <div class="loading-spinner"></div> | |
| <div class="loading-text">Fetching latest games...</div> | |
| </div> | |
| <!-- Empty State --> | |
| <div class="empty-state" id="empty-state"> | |
| <div class="empty-icon">🏀</div> | |
| <div class="empty-title">No Games Today</div> | |
| <div class="empty-desc">Check back later for upcoming games or view the schedule.</div> | |
| </div> | |
| <!-- Games Grid --> | |
| <div class="games-grid" id="games-grid"></div> | |
| <!-- Raw Output Fallback --> | |
| <div class="raw-output" id="raw-output"></div> | |
| <!-- Available Scripts --> | |
| <div class="scripts-section"> | |
| <div class="scripts-title">Available Tools</div> | |
| <div class="scripts-grid"> | |
| <div class="script-card" onclick="runScript('nba_schedule.py')"> | |
| <div class="script-icon orange"> | |
| <i class="fas fa-calendar-alt"></i> | |
| </div> | |
| <div class="script-name">Game Schedule</div> | |
| <div class="script-desc">View today's NBA games</div> | |
| </div> | |
| <div class="script-card" onclick="runScript('nba_standings.py')"> | |
| <div class="script-icon blue"> | |
| <i class="fas fa-list-ol"></i> | |
| </div> | |
| <div class="script-name">Standings</div> | |
| <div class="script-desc">Conference standings</div> | |
| </div> | |
| <div class="script-card" onclick="runScript('nba_leaders.py')"> | |
| <div class="script-icon purple"> | |
| <i class="fas fa-star"></i> | |
| </div> | |
| <div class="script-name">League Leaders</div> | |
| <div class="script-desc">Top performers</div> | |
| </div> | |
| <div class="script-card" onclick="runScript('nba_injuries.py')"> | |
| <div class="script-icon green"> | |
| <i class="fas fa-medkit"></i> | |
| </div> | |
| <div class="script-name">Injury Report</div> | |
| <div class="script-desc">Player injuries & status</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Settings Modal --> | |
| <div class="modal-overlay" id="settings-modal"> | |
| <div class="modal"> | |
| <div class="modal-header"> | |
| <div class="modal-title">⚙️ Settings</div> | |
| <button class="modal-close" onclick="toggleSettings()"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| <div class="form-group"> | |
| <label class="form-label">Backend URL</label> | |
| <input type="text" class="form-input" id="api-url" placeholder="https://your-app.up.railway.app"> | |
| </div> | |
| <button class="modal-btn" onclick="saveSettings()"> | |
| Save & Connect | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Dev Panel --> | |
| <div class="dev-panel" id="dev-panel"> | |
| <div class="dev-header"> | |
| <div class="dev-title"> | |
| <i class="fas fa-terminal"></i> | |
| Code Editor | |
| </div> | |
| <div class="dev-actions"> | |
| <button class="dev-btn run" onclick="executeCode()"> | |
| <i class="fas fa-play"></i> Run | |
| </button> | |
| <button class="dev-btn close" onclick="toggleDev()"> | |
| <i class="fas fa-times"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <textarea id="editor"></textarea> | |
| </div> | |
| <script> | |
| // Initialize CodeMirror | |
| const editor = CodeMirror.fromTextArea(document.getElementById("editor"), { | |
| mode: "python", | |
| theme: "dracula", | |
| lineNumbers: true, | |
| lineWrapping: true | |
| }); | |
| // DOM Elements | |
| const loader = document.getElementById("loader"); | |
| const gamesGrid = document.getElementById("games-grid"); | |
| const rawOutput = document.getElementById("raw-output"); | |
| const emptyState = document.getElementById("empty-state"); | |
| const liveBadge = document.getElementById("live-badge"); | |
| const modal = document.getElementById("settings-modal"); | |
| // Set current date | |
| document.getElementById("current-date").textContent = new Date().toLocaleDateString('en-US', { | |
| weekday: 'long', | |
| year: 'numeric', | |
| month: 'long', | |
| day: 'numeric' | |
| }); | |
| // Team abbreviation mapping | |
| const teamAbbreviations = { | |
| 'Hawks': 'ATL', 'Celtics': 'BOS', 'Nets': 'BKN', 'Hornets': 'CHA', | |
| 'Bulls': 'CHI', 'Cavaliers': 'CLE', 'Mavericks': 'DAL', 'Nuggets': 'DEN', | |
| 'Pistons': 'DET', 'Warriors': 'GSW', 'Rockets': 'HOU', 'Pacers': 'IND', | |
| 'Clippers': 'LAC', 'Lakers': 'LAL', 'Grizzlies': 'MEM', 'Heat': 'MIA', | |
| 'Bucks': 'MIL', 'Timberwolves': 'MIN', 'Pelicans': 'NOP', 'Knicks': 'NYK', | |
| 'Thunder': 'OKC', 'Magic': 'ORL', '76ers': 'PHI', 'Suns': 'PHX', | |
| 'Trail Blazers': 'POR', 'Blazers': 'POR', 'Kings': 'SAC', 'Spurs': 'SAS', | |
| 'Raptors': 'TOR', 'Jazz': 'UTA', 'Wizards': 'WAS' | |
| }; | |
| function getTeamAbbr(teamName) { | |
| for (const [name, abbr] of Object.entries(teamAbbreviations)) { | |
| if (teamName.toLowerCase().includes(name.toLowerCase())) { | |
| return abbr; | |
| } | |
| } | |
| return 'DEFAULT'; | |
| } | |
| // On Load | |
| window.onload = function() { | |
| const savedUrl = localStorage.getItem("backend_url"); | |
| if (!savedUrl) { | |
| toggleSettings(); | |
| } else { | |
| document.getElementById("api-url").value = savedUrl; | |
| runScript('nba_schedule.py'); | |
| } | |
| }; | |
| // Parse game data from output | |
| function parseGames(output) { | |
| // This is a flexible parser - adjust based on your actual output format | |
| const games = []; | |
| const lines = output.split('\n').filter(line => line.trim()); | |
| // Try to detect game patterns (customize based on your script output) | |
| let currentGame = null; | |
| for (const line of lines) { | |
| // Pattern: "Team1 vs Team2" or "Team1 @ Team2" | |
| const vsMatch = line.match(/(.+?)\s+(vs\.?|@)\s+(.+)/i); | |
| if (vsMatch) { | |
| currentGame = { | |
| away: { name: vsMatch[1].trim(), score: null }, | |
| home: { name: vsMatch[3].trim(), score: null }, | |
| status: 'upcoming', | |
| time: '', | |
| channel: '' | |
| }; | |
| games.push(currentGame); | |
| continue; | |
| } | |
| // Pattern: Score line "100 - 98" or similar | |
| const scoreMatch = line.match(/(\d+)\s*[-–]\s*(\d+)/); | |
| if (scoreMatch && currentGame) { | |
| currentGame.away.score = parseInt(scoreMatch[1]); | |
| currentGame.home.score = parseInt(scoreMatch[2]); | |
| currentGame.status = 'live'; | |
| } | |
| // Pattern: Time like "7:00 PM ET" | |
| const timeMatch = line.match(/(\d{1,2}:\d{2}\s*(AM|PM)\s*(ET|PT|CT)?)/i); | |
| if (timeMatch && currentGame) { | |
| currentGame.time = timeMatch[1]; | |
| } | |
| // Pattern: Final | |
| if (line.toLowerCase().includes('final') && currentGame) { | |
| currentGame.status = 'final'; | |
| } | |
| // Pattern: LIVE or In Progress | |
| if ((line.toLowerCase().includes('live') || line.toLowerCase().includes('in progress')) && currentGame) { | |
| currentGame.status = 'live'; | |
| } | |
| } | |
| return games; | |
| } | |
| // Render game cards | |
| function renderGames(games) { | |
| if (!games || games.length === 0) { | |
| gamesGrid.style.display = 'none'; | |
| emptyState.style.display = 'flex'; | |
| liveBadge.style.display = 'none'; | |
| return; | |
| } | |
| emptyState.style.display = 'none'; | |
| gamesGrid.style.display = 'grid'; | |
| const hasLive = games.some(g => g.status === 'live'); | |
| liveBadge.style.display = hasLive ? 'flex' : 'none'; | |
| gamesGrid.innerHTML = games.map(game => { | |
| const awayAbbr = getTeamAbbr(game.away.name); | |
| const homeAbbr = getTeamAbbr(game.home.name); | |
| const awayLeading = game.away.score > game.home.score; | |
| const homeLeading = game.home.score > game.away.score; | |
| return ` | |
| <div class="game-card ${game.status === 'live' ? 'live' : ''}"> | |
| <div class="game-status"> | |
| <span class="status-badge ${game.status}">${ | |
| game.status === 'live' ? '🔴 LIVE' : | |
| game.status === 'final' ? 'FINAL' : 'UPCOMING' | |
| }</span> | |
| <span class="game-time">${game.time || ''}</span> | |
| </div> | |
| <div class="teams-container"> | |
| <div class="team-row ${game.status === 'final' && awayLeading ? 'winner' : ''}"> | |
| <div class="team-info"> | |
| <div class="team-logo team-${awayAbbr}">${awayAbbr}</div> | |
| <div class="team-details"> | |
| <div class="team-name">${game.away.name}</div> | |
| <div class="team-record">${game.away.record || ''}</div> | |
| </div> | |
| </div> | |
| <div class="team-score ${awayLeading ? 'leading' : ''}">${game.away.score ?? '-'}</div> | |
| </div> | |
| <div class="team-row ${game.status === 'final' && homeLeading ? 'winner' : ''}"> | |
| <div class="team-info"> | |
| <div class="team-logo team-${homeAbbr}">${homeAbbr}</div> | |
| <div class="team-details"> | |
| <div class="team-name">${game.home.name}</div> | |
| <div class="team-record">${game.home.record || ''}</div> | |
| </div> | |
| </div> | |
| <div class="team-score ${homeLeading ? 'leading' : ''}">${game.home.score ?? '-'}</div> | |
| </div> | |
| </div> | |
| <div class="game-footer"> | |
| <div class="game-channel"> | |
| <i class="fas fa-tv"></i> | |
| ${game.channel || 'NBA League Pass'} | |
| </div> | |
| <div class="game-actions"> | |
| <button class="action-btn"> | |
| <i class="fas fa-chart-line"></i> Stats | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| }).join(''); | |
| } | |
| // Run Script | |
| async function runScript(filename) { | |
| // Show loading | |
| loader.style.display = 'flex'; | |
| gamesGrid.style.display = 'none'; | |
| rawOutput.style.display = 'none'; | |
| emptyState.style.display = 'none'; | |
| try { | |
| // Fetch script | |
| const scriptRes = await fetch(`scripts/${filename}?t=${Date.now()}`); | |
| if (!scriptRes.ok) throw new Error("Could not load script file."); | |
| const pythonCode = await scriptRes.text(); | |
| editor.setValue(pythonCode); | |
| // Send to backend | |
| const backendUrl = localStorage.getItem("backend_url").replace(/\/$/, ""); | |
| const res = await fetch(`${backendUrl}/run`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ code: pythonCode }) | |
| }); | |
| if (!res.ok) throw new Error("Backend connection failed."); | |
| const data = await res.json(); | |
| // Try to parse as structured game data | |
| const games = parseGames(data.output); | |
| if (games.length > 0) { | |
| renderGames(games); | |
| rawOutput.style.display = 'none'; | |
| } else { | |
| // Fallback to raw output | |
| gamesGrid.style.display = 'none'; | |
| rawOutput.style.display = 'block'; | |
| rawOutput.className = data.error ? 'raw-output error' : 'raw-output'; | |
| rawOutput.textContent = data.output || 'No data returned.'; | |
| } | |
| } catch (err) { | |
| gamesGrid.style.display = 'none'; | |
| rawOutput.style.display = 'block'; | |
| rawOutput.className = 'raw-output error'; | |
| rawOutput.textContent = `⚠️ Error: ${err.message}`; | |
| } finally { | |
| loader.style.display = 'none'; | |
| } | |
| } | |
| // Execute from editor | |
| async function executeCode() { | |
| const code = editor.getValue(); | |
| const backendUrl = localStorage.getItem("backend_url").replace(/\/$/, ""); | |
| loader.style.display = 'flex'; | |
| gamesGrid.style.display = 'none'; | |
| try { | |
| const res = await fetch(`${backendUrl}/run`, { | |
| method: "POST", | |
| headers: {"Content-Type":"application/json"}, | |
| body: JSON.stringify({ code }) | |
| }); | |
| const data = await res.json(); | |
| const games = parseGames(data.output); | |
| if (games.length > 0) { | |
| renderGames(games); | |
| } else { | |
| rawOutput.style.display = 'block'; | |
| rawOutput.textContent = data.output; | |
| } | |
| } catch(e) { | |
| rawOutput.style.display = 'block'; | |
| rawOutput.className = 'raw-output error'; | |
| rawOutput.textContent = e.message; | |
| } finally { | |
| loader.style.display = 'none'; | |
| } | |
| } | |
| // Tab switching | |
| function switchTab(tab) { | |
| document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active')); | |
| event.target.closest('.tab-btn').classList.add('active'); | |
| // You can add different script calls for each tab | |
| const scriptMap = { | |
| 'games': 'nba_schedule.py', | |
| 'standings': 'nba_standings.py', | |
| 'stats': 'nba_leaders.py', | |
| 'players': 'nba_players.py' | |
| }; | |
| if (scriptMap[tab]) { | |
| runScript(scriptMap[tab]); | |
| } | |
| } | |
| // Toggle functions | |
| function toggleSettings() { | |
| modal.style.display = modal.style.display === 'flex' ? 'none' : 'flex'; | |
| } | |
| function saveSettings() { | |
| const url = document.getElementById("api-url").value; | |
| if(url) { | |
| localStorage.setItem("backend_url", url); | |
| toggleSettings(); | |
| runScript('nba_schedule.py'); | |
| } | |
| } | |
| function toggleDev() { | |
| document.getElementById("dev-panel").classList.toggle("open"); | |
| setTimeout(() => editor.refresh(), 300); | |
| } | |
| // Close modal on overlay click | |
| modal.addEventListener('click', (e) => { | |
| if (e.target === modal) toggleSettings(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |