Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
| <title>NeoChess</title> | |
| <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> | |
| <link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css"> | |
| <script src="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.3/chess.min.js"></script> | |
| <script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --bg-body: #050505; | |
| --bg-main: #0B0C10; | |
| --bg-surface: rgba(25, 28, 36, 0.65); | |
| --bg-surface-light: rgba(36, 40, 51, 0.85); | |
| --green: #10B981; | |
| --green-hover: #34D399; | |
| --green-dark: #047857; | |
| --green-glow: rgba(16, 185, 129, 0.4); | |
| --text-main: #F3F4F6; | |
| --text-muted: #9CA3AF; | |
| --text-light: #D1D5DB; | |
| --border: rgba(255, 255, 255, 0.08); | |
| --border-highlight: rgba(255, 255, 255, 0.15); | |
| --danger: #EF4444; | |
| --danger-glow: rgba(239, 68, 68, 0.4); | |
| --warning: #F59E0B; | |
| --info: #3B82F6; | |
| --brilliant: #1baca6; | |
| --great: #5c8bb0; | |
| --best: #81b64c; | |
| --excel: #96bc4b; | |
| --good: #789936; | |
| --inacc: #f5c04e; | |
| --mistake: #e58f2a; | |
| --blunder: #ca3431; | |
| --glass-blur: blur(16px); | |
| } | |
| * { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, Helvetica, Arial, sans-serif; -webkit-tap-highlight-color: transparent; } | |
| body { | |
| background-color: var(--bg-body); | |
| background-image: radial-gradient(circle at 50% 0%, rgba(16, 185, 129, 0.05) 0%, transparent 50%); | |
| color: var(--text-main); | |
| height: 100dvh; | |
| overflow: hidden; | |
| display: flex; | |
| justify-content: center; | |
| } | |
| ::-webkit-scrollbar { width: 6px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--bg-surface-light); border-radius: 10px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--text-muted); } | |
| .app-container { | |
| width: 100%; | |
| max-width: 480px; | |
| height: 100dvh; | |
| background-color: var(--bg-main); | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| box-shadow: 0 0 50px rgba(0,0,0,0.8); | |
| overflow: hidden; | |
| border-left: 1px solid var(--border); | |
| border-right: 1px solid var(--border); | |
| } | |
| .header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| padding: 16px 20px; | |
| background: var(--bg-surface); | |
| backdrop-filter: var(--glass-blur); | |
| -webkit-backdrop-filter: var(--glass-blur); | |
| border-bottom: 1px solid var(--border); | |
| flex-shrink: 0; | |
| z-index: 10; | |
| } | |
| .header-logo { color: var(--text-main); font-size: 22px; font-weight: 900; display: flex; align-items: center; gap: 10px; letter-spacing: -0.5px; } | |
| .header-logo i { color: var(--green); font-size: 24px; filter: drop-shadow(0 0 8px var(--green-glow)); } | |
| .auth-btns button { background: none; border: none; color: var(--text-light); font-size: 15px; font-weight: 700; cursor: pointer; padding: 8px 12px; transition: color 0.2s; } | |
| .auth-btns button:hover { color: white; } | |
| .auth-btns .btn-signup { | |
| background: linear-gradient(135deg, var(--green), var(--green-dark)); | |
| color: white; | |
| border-radius: 10px; | |
| margin-left: 8px; | |
| padding: 8px 16px; | |
| box-shadow: 0 4px 15px var(--green-glow), inset 0 1px 1px rgba(255,255,255,0.2); | |
| transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1); | |
| } | |
| .auth-btns .btn-signup:active { transform: scale(0.95); box-shadow: 0 2px 5px var(--green-glow); } | |
| .view { display: none; flex: 1; overflow-y: auto; padding-bottom: 90px; } | |
| .view.active { display: block; animation: fadeinView 0.3s ease-out; } | |
| @keyframes fadeinView { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
| #view-game.active { display: flex; flex-direction: column; padding-bottom: 0; overflow: hidden;} | |
| .bottom-nav { | |
| position: absolute; | |
| bottom: 20px; | |
| left: 20px; | |
| right: 20px; | |
| height: 70px; | |
| background: rgba(25, 28, 36, 0.85); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| display: flex; | |
| justify-content: space-around; | |
| align-items: center; | |
| border: 1px solid var(--border-highlight); | |
| border-radius: 24px; | |
| box-shadow: 0 15px 35px rgba(0,0,0,0.5); | |
| z-index: 100; | |
| } | |
| .nav-item { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| color: var(--text-muted); | |
| font-size: 11px; | |
| font-weight: 700; | |
| cursor: pointer; | |
| background: none; | |
| border: none; | |
| width: 20%; | |
| gap: 6px; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .nav-item i { font-size: 22px; transition: all 0.3s ease; } | |
| .nav-item.active { color: var(--green); transform: translateY(-4px); } | |
| .nav-item.active i { filter: drop-shadow(0 0 10px var(--green-glow)); transform: scale(1.1); } | |
| .btn-smart { | |
| display: flex; | |
| width: calc(100% - 40px); | |
| margin: 15px auto; | |
| background: linear-gradient(135deg, var(--green), var(--green-dark)); | |
| color: white; | |
| text-align: center; | |
| padding: 16px; | |
| border-radius: 14px; | |
| font-size: 18px; | |
| font-weight: 800; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| cursor: pointer; | |
| box-shadow: 0 8px 20px var(--green-glow), inset 0 1px 1px rgba(255,255,255,0.2); | |
| transition: all 0.2s cubic-bezier(0.2, 0.8, 0.2, 1); | |
| justify-content: center; | |
| align-items: center; | |
| gap: 12px; | |
| letter-spacing: 0.5px; | |
| } | |
| .btn-smart:active { transform: scale(0.97); box-shadow: 0 4px 10px var(--green-glow); } | |
| .gray-btn { | |
| background: linear-gradient(180deg, var(--bg-surface-light), rgba(25, 28, 36, 1)); | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.3), inset 0 1px 1px rgba(255,255,255,0.05); | |
| color: white; | |
| padding: 16px; | |
| border-radius: 14px; | |
| border: 1px solid var(--border); | |
| font-size: 16px; | |
| font-weight: 700; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| cursor: pointer; | |
| justify-content: center; | |
| width: calc(100% - 40px); | |
| margin: 10px auto; | |
| transition: all 0.2s; | |
| } | |
| .gray-btn:active { transform: scale(0.97); } | |
| .home-menu { padding: 20px; } | |
| .menu-item { | |
| display: flex; | |
| align-items: center; | |
| padding: 14px; | |
| background: var(--bg-surface); | |
| border: 1px solid var(--border); | |
| border-radius: 16px; | |
| margin-bottom: 15px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| box-shadow: 0 4px 15px rgba(0,0,0,0.2); | |
| } | |
| .menu-item:hover, .menu-item:active { | |
| background: var(--bg-surface-light); | |
| transform: translateY(-2px); | |
| border-color: var(--border-highlight); | |
| box-shadow: 0 8px 25px rgba(0,0,0,0.4); | |
| } | |
| .menu-icon-box { | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 14px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| margin-right: 18px; | |
| background: rgba(255,255,255,0.05); | |
| border: 1px solid var(--border); | |
| box-shadow: inset 0 2px 10px rgba(0,0,0,0.2); | |
| } | |
| .menu-icon-box i { font-size: 28px; color: var(--text-light); } | |
| .menu-item:nth-child(1) .menu-icon-box i { color: var(--green); filter: drop-shadow(0 0 8px var(--green-glow)); } | |
| .menu-item:nth-child(2) .menu-icon-box i { color: var(--warning); filter: drop-shadow(0 0 8px rgba(245, 158, 11, 0.4)); } | |
| .menu-item:nth-child(3) .menu-icon-box i { color: var(--info); filter: drop-shadow(0 0 8px rgba(59, 130, 246, 0.4)); } | |
| .menu-text h3 { font-size: 18px; font-weight: 800; margin-bottom: 4px; color: var(--text-main); letter-spacing: -0.3px;} | |
| .menu-text p { font-size: 14px; color: var(--text-muted); font-weight: 500;} | |
| .profile-header { padding: 25px 20px; display: flex; flex-direction: column; gap: 16px; background: linear-gradient(180deg, var(--bg-surface), var(--bg-main)); border-bottom: 1px solid var(--border);} | |
| .prof-top { display: flex; align-items: center; gap: 20px; position: relative;} | |
| .prof-top img { width: 85px; height: 85px; border-radius: 24px; object-fit: cover; background: #fff; border: 2px solid var(--border-highlight); box-shadow: 0 10px 20px rgba(0,0,0,0.5); } | |
| .status-dot { position: absolute; bottom: -4px; left: 65px; width: 22px; height: 22px; border-radius: 50%; border: 4px solid var(--bg-main); background: var(--text-muted); display:flex; align-items:center; justify-content:center; box-shadow: 0 0 10px rgba(0,0,0,0.5); transition: background 0.3s;} | |
| .status-dot.online { background: var(--green); box-shadow: 0 0 15px var(--green-glow); animation: pulse-green 2s infinite;} | |
| @keyframes pulse-green { 0% { box-shadow: 0 0 0 0 var(--green-glow); } 70% { box-shadow: 0 0 0 10px rgba(16,185,129,0); } 100% { box-shadow: 0 0 0 0 rgba(16,185,129,0); } } | |
| .prof-info { flex: 1; } | |
| .prof-info h2 { font-size: 24px; display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-weight: 900; letter-spacing: -0.5px;} | |
| .prof-info p { color: var(--text-muted); font-size: 14px; font-weight: 500;} | |
| .prof-bio-text { font-size: 15px; color: var(--text-light); margin-top: 5px; line-height: 1.5; padding: 14px; background: var(--bg-surface-light); border-radius: 12px; border-left: 4px solid var(--green); box-shadow: inset 0 2px 10px rgba(0,0,0,0.2);} | |
| .prof-actions { display: flex; gap: 12px; margin-top: 10px; } | |
| .btn-add-friend { flex: 1; padding: 14px; background: var(--bg-surface-light); color: white; border: 1px solid var(--border); border-radius: 12px; font-weight: 700; font-size: 15px; cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 8px; transition: all 0.2s;} | |
| .btn-add-friend:active { transform: scale(0.96); } | |
| .btn-challenge { flex: 1; padding: 14px; background: linear-gradient(135deg, var(--green), var(--green-dark)); color: white; border: none; border-radius: 12px; font-weight: 800; font-size: 15px; cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 8px; box-shadow: 0 4px 15px var(--green-glow); transition: all 0.2s;} | |
| .btn-challenge:active { transform: scale(0.96); box-shadow: 0 2px 8px var(--green-glow); } | |
| .stats-section { padding: 20px; } | |
| .stats-section-title { font-size: 18px; font-weight: 900; margin-bottom: 16px; display: flex; justify-content: space-between; color: var(--text-main); align-items: center;} | |
| .stats-section-title span { font-size: 14px; font-weight: 600; color: var(--green); cursor: pointer; background: rgba(16, 185, 129, 0.1); padding: 4px 10px; border-radius: 20px;} | |
| .stats-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; } | |
| .stat-box { background: var(--bg-surface); border: 1px solid var(--border); padding: 18px 10px; border-radius: 16px; text-align: center; box-shadow: 0 4px 12px rgba(0,0,0,0.2); transition: transform 0.2s;} | |
| .stat-box:hover { transform: translateY(-3px); border-color: var(--border-highlight); } | |
| .stat-box i { font-size: 24px; margin-bottom: 10px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.5));} | |
| .stat-box h4 { font-size: 20px; font-weight: 900; letter-spacing: -0.5px;} | |
| .stat-box p { font-size: 13px; color: var(--text-muted); font-weight: 600; margin-top: 4px; text-transform: uppercase; letter-spacing: 0.5px;} | |
| .game-history-list { display: flex; flex-direction: column; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 16px; overflow: hidden; box-shadow: 0 8px 20px rgba(0,0,0,0.3);} | |
| .history-card { display: flex; justify-content: space-between; align-items: center; padding: 14px 16px; border-bottom: 1px solid var(--border); cursor: pointer; transition: background 0.2s;} | |
| .history-card:last-child { border-bottom: none; } | |
| .history-card:hover, .history-card:active { background: var(--bg-surface-light); } | |
| .hist-left { display: flex; align-items: center; gap: 14px; flex: 1; } | |
| .hist-icon-wrap { font-size: 20px; display: flex; align-items: center; justify-content: center; width: 28px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));} | |
| .hist-avatar { width: 38px; height: 38px; border-radius: 10px; object-fit: cover; border: 1px solid var(--border-highlight);} | |
| .hist-details { display: flex; flex-direction: column; } | |
| .hist-details strong { font-size: 16px; color: var(--text-main); font-weight: 700;} | |
| .hist-details span { font-size: 12px; color: var(--text-muted); margin-top: 4px; font-weight: 500;} | |
| .hist-right { display: flex; align-items: center; gap: 14px; } | |
| .hist-symbol { font-weight: 900; font-size: 16px; width: 24px; height: 24px; border-radius: 6px; display: flex; align-items: center; justify-content: center; box-shadow: inset 0 1px 3px rgba(0,0,0,0.3);} | |
| .hist-symbol.win { background: rgba(16, 185, 129, 0.15); color: var(--green); border: 1px solid rgba(16, 185, 129, 0.3);} | |
| .hist-symbol.loss { background: rgba(239, 68, 68, 0.15); color: var(--danger); border: 1px solid rgba(239, 68, 68, 0.3);} | |
| .hist-symbol.draw { background: rgba(156, 163, 175, 0.15); color: var(--text-muted); border: 1px solid rgba(156, 163, 175, 0.3);} | |
| .hist-accuracy { font-size: 14px; font-weight: 800; width: 50px; text-align: center; padding: 6px 0; border-radius: 8px; border: 1px solid rgba(16, 185, 129, 0.2); box-shadow: inset 0 2px 5px rgba(0,0,0,0.2);} | |
| .more-list { list-style: none; padding: 10px 20px; } | |
| .more-list li { padding: 16px 20px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 14px; margin-bottom: 10px; font-size: 16px; font-weight: 700; display: flex; align-items: center; gap: 16px; cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 10px rgba(0,0,0,0.2);} | |
| .more-list li:hover, .more-list li:active { background: var(--bg-surface-light); transform: translateY(-2px); border-color: var(--border-highlight);} | |
| .more-list li i { width: 28px; text-align: center; font-size: 20px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.4));} | |
| .leaderboard-tabs { display: flex; padding: 10px 20px; background: var(--bg-main); gap: 10px; } | |
| .leaderboard-tabs button { flex: 1; padding: 12px; background: var(--bg-surface); border: 1px solid var(--border); border-radius: 12px; color: var(--text-muted); font-weight: 800; font-size: 15px; cursor: pointer; transition: all 0.2s; box-shadow: 0 2px 5px rgba(0,0,0,0.2);} | |
| .leaderboard-tabs button.active { background: var(--green); color: white; border-color: var(--green-dark); box-shadow: 0 4px 15px var(--green-glow); } | |
| .game-top, .game-bottom { display: flex; justify-content: space-between; align-items: center; padding: 14px 20px; background: var(--bg-main); flex-shrink: 0; z-index: 10;} | |
| .player-tag { display: flex; align-items: center; gap: 12px; cursor: pointer; flex: 1; overflow: hidden; background: var(--bg-surface); padding: 8px 12px; border-radius: 12px; border: 1px solid var(--border); box-shadow: 0 4px 12px rgba(0,0,0,0.3);} | |
| .player-tag img { width: 44px; height: 44px; border-radius: 10px; background: #fff; object-fit: cover; border: 1px solid var(--border-highlight);} | |
| .player-tag-info { display: flex; flex-direction: column; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;} | |
| .player-tag-info strong { font-size: 16px; font-weight: 800; color: var(--text-main); display: flex; align-items: center; gap: 6px; overflow: hidden; text-overflow: ellipsis;} | |
| .player-tag-info span { font-size: 13px; color: var(--text-muted); font-weight: 600; display: flex; gap: 6px; margin-top: 3px;} | |
| .clock { | |
| background: linear-gradient(180deg, #2A2D35, #1E2026); | |
| padding: 12px 16px; | |
| font-family: "SF Mono", "Roboto Mono", monospace; | |
| font-size: 20px; | |
| font-weight: 900; | |
| border-radius: 12px; | |
| color: white; | |
| border: 1px solid rgba(255,255,255,0.1); | |
| min-width: 90px; | |
| text-align: center; | |
| margin-left: 12px; | |
| box-shadow: inset 0 2px 10px rgba(0,0,0,0.8), 0 4px 10px rgba(0,0,0,0.3); | |
| letter-spacing: 1px; | |
| transition: all 0.3s; | |
| } | |
| .clock.low { | |
| background: linear-gradient(180deg, rgba(239, 68, 68, 0.3), rgba(153, 27, 27, 0.8)); | |
| color: #FECACA; | |
| border-color: var(--danger); | |
| box-shadow: inset 0 2px 10px rgba(0,0,0,0.8), 0 0 15px var(--danger-glow); | |
| animation: pulse-danger 1s infinite; | |
| } | |
| @keyframes pulse-danger { 0% { box-shadow: inset 0 2px 10px rgba(0,0,0,0.8), 0 0 5px var(--danger-glow); } 50% { box-shadow: inset 0 2px 10px rgba(0,0,0,0.8), 0 0 20px var(--danger); } 100% { box-shadow: inset 0 2px 10px rgba(0,0,0,0.8), 0 0 5px var(--danger-glow); } } | |
| #board { width: 100%; background: var(--bg-main); aspect-ratio: 1/1; position: relative; flex-shrink: 0; box-shadow: 0 0 30px rgba(0,0,0,0.8); z-index: 5;} | |
| .game-actions-bar { display: flex; justify-content: space-around; align-items: center; padding: 15px 10px; background: var(--bg-surface); flex: 1; border-top: 1px solid var(--border); backdrop-filter: blur(10px);} | |
| .action-icon-col { display: flex; flex-direction: column; align-items: center; gap: 6px; color: var(--text-muted); cursor: pointer; font-size: 12px; font-weight: 800; transition: all 0.2s;} | |
| .action-icon-col:hover { color: var(--text-main); transform: translateY(-2px);} | |
| .action-icon-col i { font-size: 22px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));} | |
| .action-icon-col.active-star i { color: white; background: linear-gradient(135deg, var(--green), var(--green-dark)); padding: 12px; border-radius: 12px; box-shadow: 0 4px 15px var(--green-glow); margin-top: -10px; font-size: 20px;} | |
| .chat-overlay { position: absolute; bottom: 0; left: 0; width: 100%; height: 50vh; background: rgba(20, 22, 28, 0.95); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); z-index: 1000; border-top-left-radius: 24px; border-top-right-radius: 24px; display: flex; flex-direction: column; box-shadow: 0 -10px 40px rgba(0,0,0,0.8); border-top: 1px solid var(--border-highlight); transform: translateY(100%); transition: transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);} | |
| .chat-overlay.open { transform: translateY(0); } | |
| .chat-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; font-weight: 900; font-size: 16px; border-bottom: 1px solid var(--border);} | |
| .chat-messages { flex: 1; padding: 20px; overflow-y: auto; font-size: 15px; display: flex; flex-direction: column; gap: 12px;} | |
| .chat-input-row { display: flex; padding: 15px 20px; background: rgba(0,0,0,0.2); gap: 12px; border-top: 1px solid var(--border);} | |
| .chat-input-row input { flex: 1; padding: 14px 20px; border-radius: 24px; border: 1px solid var(--border); background: var(--bg-surface); color: white; outline: none; font-size: 15px; box-shadow: inset 0 2px 6px rgba(0,0,0,0.3); transition: border 0.2s;} | |
| .chat-input-row input:focus { border-color: var(--green); } | |
| .chat-input-row button { background: linear-gradient(135deg, var(--green), var(--green-dark)); border: none; color: white; width: 46px; height: 46px; border-radius: 50%; cursor: pointer; flex-shrink: 0; box-shadow: 0 4px 10px var(--green-glow); font-size: 18px; transition: transform 0.2s;} | |
| .chat-input-row button:active { transform: scale(0.9); } | |
| .full-overlay { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(11, 12, 16, 0.95); backdrop-filter: blur(15px); -webkit-backdrop-filter: blur(15px); z-index: 2000; flex-direction: column; animation: fadeinOverlay 0.3s ease-out;} | |
| @keyframes fadeinOverlay { from { opacity: 0; } to { opacity: 1; } } | |
| .overlay-header { padding: 20px; display: flex; align-items: center; gap: 15px; font-size: 20px; font-weight: 900; border-bottom: 1px solid var(--border); background: transparent; flex-shrink: 0;} | |
| .overlay-header i { cursor: pointer; font-size: 22px; padding: 6px; color: var(--text-light); transition: color 0.2s; background: rgba(255,255,255,0.05); border-radius: 50%; width: 36px; height: 36px; display: flex; align-items: center; justify-content: center;} | |
| .overlay-header i:hover { color: white; background: rgba(255,255,255,0.1); } | |
| .overlay-content { padding: 25px 20px; display: flex; flex-direction: column; gap: 18px; overflow-y: auto; flex: 1;} | |
| .input-box { width: 100%; padding: 16px 20px; background: rgba(255,255,255,0.03); border: 1px solid var(--border); color: white; border-radius: 14px; font-size: 16px; outline: none; transition: all 0.3s; box-shadow: inset 0 2px 8px rgba(0,0,0,0.2);} | |
| .input-box:focus { border-color: var(--green); background: rgba(16, 185, 129, 0.05); box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1), inset 0 2px 8px rgba(0,0,0,0.2);} | |
| .search-screen { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(11, 12, 16, 0.98); backdrop-filter: blur(20px); z-index: 3000; flex-direction: column; justify-content: center; align-items: center; } | |
| .spinner { width: 70px; height: 70px; border: 4px solid var(--border); border-top-color: var(--green); border-radius: 50%; animation: spin 1s cubic-bezier(0.5, 0, 0.5, 1) infinite; margin-bottom: 25px; box-shadow: 0 0 20px rgba(16, 185, 129, 0.2);} | |
| @keyframes spin { 100% { transform: rotate(360deg); } } | |
| .search-time { font-size: 32px; font-weight: 900; font-family: "SF Mono", monospace; color: var(--green); text-shadow: 0 0 15px var(--green-glow); margin-bottom: 10px;} | |
| .review-modal-bg { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); backdrop-filter: blur(10px); z-index: 4000; flex-direction: column; justify-content: center; align-items: center; padding: 20px; animation: fadeinOverlay 0.3s ease-out;} | |
| .review-modal { background: var(--bg-surface-light); border: 1px solid var(--border-highlight); width: 100%; max-width: 400px; border-radius: 24px; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 25px 50px rgba(0,0,0,0.8), 0 0 0 1px rgba(255,255,255,0.05); animation: popModal 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);} | |
| @keyframes popModal { 0% { transform: scale(0.9); opacity: 0; } 100% { transform: scale(1); opacity: 1; } } | |
| .rm-header { padding: 30px 20px 20px; text-align: center; position: relative; border-bottom: 1px solid var(--border); background: linear-gradient(180deg, rgba(255,255,255,0.05), transparent);} | |
| .rm-close { position: absolute; top: 20px; right: 20px; font-size: 24px; color: var(--text-muted); cursor: pointer; transition: color 0.2s; background: rgba(0,0,0,0.2); width: 36px; height: 36px; display:flex; align-items:center; justify-content:center; border-radius:50%;} | |
| .rm-close:hover { color: white; background: rgba(0,0,0,0.4); } | |
| .rm-title { font-size: 32px; font-weight: 900; letter-spacing: -1px; text-shadow: 0 2px 10px rgba(0,0,0,0.5);} | |
| .rm-subtitle { font-size: 15px; font-weight: 700; color: var(--text-muted); margin-top: 8px; text-transform: uppercase; letter-spacing: 1px;} | |
| .rm-coach-section { padding: 25px 20px; display: flex; gap: 15px; align-items: flex-start; } | |
| .rm-coach-img { width: 64px; height: 64px; filter: drop-shadow(0 4px 10px rgba(0,0,0,0.5)); animation: floatCoach 3s ease-in-out infinite;} | |
| @keyframes floatCoach { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-5px); } } | |
| .rm-bubble { background: linear-gradient(135deg, #ffffff, #f0f0f0); color: #111; padding: 14px 18px; border-radius: 16px; font-weight: 700; font-size: 15px; position: relative; flex: 1; border-top-left-radius: 0; display:flex; align-items:center; min-height:64px; box-shadow: 0 8px 20px rgba(0,0,0,0.4);} | |
| .rm-bubble::before { content:''; position:absolute; top: 0; left: -10px; border-width: 0 12px 14px 0; border-style: solid; border-color: transparent #ffffff transparent transparent; filter: drop-shadow(-2px 0 2px rgba(0,0,0,0.1));} | |
| .rm-stats { display: flex; justify-content: space-around; padding: 0 20px 25px 20px; } | |
| .rm-stat-item { display: flex; flex-direction: column; align-items: center; font-weight: 800; font-size: 13px; letter-spacing: 0.5px;} | |
| .rm-stat-item.great { color: var(--great); text-shadow: 0 0 10px rgba(96, 165, 250, 0.4);} | |
| .rm-stat-item.best { color: var(--best); text-shadow: 0 0 10px rgba(16, 185, 129, 0.4);} | |
| .rm-stat-item.excellent { color: var(--excel); text-shadow: 0 0 10px rgba(167, 243, 208, 0.4);} | |
| .rm-stat-num { font-size: 26px; display: flex; align-items: center; gap: 8px; font-weight: 900; margin-bottom: 6px;} | |
| .rm-stat-num i { font-size: 14px; background: currentColor; color: #111; border-radius: 50%; width: 24px; height: 24px; display: flex; justify-content: center; align-items: center; box-shadow: 0 2px 8px currentColor;} | |
| .rm-actions { padding: 0 20px 25px 20px; display: flex; flex-direction: column; gap: 12px;} | |
| .rm-btn-review { background: linear-gradient(135deg, var(--green), var(--green-dark)); color: white; padding: 16px; border-radius: 14px; font-size: 18px; font-weight: 900; border: 1px solid rgba(255,255,255,0.2); text-align: center; box-shadow: 0 8px 20px var(--green-glow); cursor: pointer; display:flex; align-items:center; justify-content:center; gap:10px; transition: all 0.2s;} | |
| .rm-btn-review:active { transform: scale(0.96); box-shadow: 0 4px 10px var(--green-glow); } | |
| .rm-actions-row { display: flex; gap: 12px; } | |
| .rm-btn-gray { flex: 1; background: rgba(255,255,255,0.05); color: white; padding: 14px; border-radius: 12px; font-weight: 800; font-size: 15px; border: 1px solid var(--border); box-shadow: 0 4px 10px rgba(0,0,0,0.2); cursor: pointer; transition: all 0.2s; display:flex; align-items:center; justify-content:center; gap:8px;} | |
| .rm-btn-gray:hover { background: rgba(255,255,255,0.1); } | |
| .rm-btn-gray:active { transform: scale(0.96); } | |
| .endgame-icon { position: absolute; z-index: 100; display: flex; justify-content: center; align-items: center; width: 60%; height: 60%; animation: popin 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); } | |
| .endgame-icon.crown { color: var(--warning); top: -12px; left: 20%; filter: drop-shadow(0 4px 6px rgba(0,0,0,0.8)); font-size: 28px; text-shadow: 0 0 15px rgba(245, 158, 11, 0.6);} | |
| .endgame-icon.lose { color: white; top: 20%; left: 20%; background: linear-gradient(135deg, #EF4444, #991B1B); border-radius: 50%; width: 60%; height: 60%; font-size: 20px; border: 2px solid white; box-shadow: 0 4px 12px rgba(0,0,0,0.8);} | |
| .endgame-icon.lose-resign { background: linear-gradient(135deg, #fff, #ddd); color: #111; font-size: 20px; } | |
| .endgame-icon.lose-abandon { background: linear-gradient(135deg, #4B5563, #1F2937); color: white; font-size: 20px; } | |
| .analysis-badge { position: absolute; top: -6px; right: -6px; color: white; font-weight: 900; font-size: 13px; width: 26px; height: 26px; border-radius: 50%; display: flex; align-items: center; justify-content: center; z-index: 100; box-shadow: 0 4px 10px rgba(0,0,0,0.6); animation: popin 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); border: 2px solid #111; } | |
| @keyframes popin { 0% { transform: scale(0.3) rotate(-10deg); opacity: 0; } 100% { transform: scale(1) rotate(0); opacity: 1; } } | |
| .time-grid-title { color: var(--text-muted); font-size: 14px; font-weight: 800; margin: 20px 0 10px 0; text-transform: uppercase; letter-spacing: 1px; display:flex; align-items:center; gap:8px;} | |
| .time-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 12px; } | |
| .time-box { background: rgba(255,255,255,0.03); padding: 18px 10px; border-radius: 12px; text-align: center; color: white; font-weight: 800; font-size: 16px; border: 2px solid var(--border); cursor: pointer; transition: all 0.2s; box-shadow: 0 4px 10px rgba(0,0,0,0.2);} | |
| .time-box:hover { background: rgba(255,255,255,0.08); transform: translateY(-2px);} | |
| .time-box.active { border-color: var(--green); background: rgba(16, 185, 129, 0.15); box-shadow: 0 4px 15px rgba(16, 185, 129, 0.2), inset 0 0 10px rgba(16, 185, 129, 0.1); color: var(--green);} | |
| #toast { visibility: hidden; min-width: 250px; background: rgba(20, 22, 28, 0.95); backdrop-filter: blur(10px); color: #fff; text-align: center; border-radius: 30px; padding: 16px 24px; position: fixed; z-index: 9999; left: 50%; bottom: 100px; transform: translateX(-50%); font-size: 15px; font-weight: 800; box-shadow: 0 10px 30px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.1); border-bottom: 2px solid var(--green); } | |
| #toast.show { visibility: visible; animation: popIsland 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), fadeoutIsland 0.4s 2.5s forwards; } | |
| @keyframes popIsland { 0% { bottom: 80px; opacity: 0; transform: translateX(-50%) scale(0.8); } 100% { bottom: 100px; opacity: 1; transform: translateX(-50%) scale(1); } } | |
| @keyframes fadeoutIsland { 0% { bottom: 100px; opacity: 1; transform: translateX(-50%) scale(1); } 100% { bottom: 120px; opacity: 0; transform: translateX(-50%) scale(0.8); visibility: hidden;} } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app-container"> | |
| <header class="header" id="main-header"> | |
| <div class="auth-btns" id="auth-unlogged"> | |
| <button onclick="openOverlay('overlay-login')">Log In</button> | |
| </div> | |
| <div class="header-logo"><i class="fa-solid fa-chess-knight"></i> NeoChess</div> | |
| <div class="auth-btns" id="auth-unlogged-2"> | |
| <button class="btn-signup" onclick="openOverlay('overlay-signup')">Sign Up</button> | |
| </div> | |
| </header> | |
| <section id="view-home" class="view active"> | |
| <div class="home-menu"> | |
| <div class="menu-item" onclick="checkAuthAndGo('view-play')"> | |
| <div class="menu-icon-box"><i class="fa-solid fa-handshake"></i></div> | |
| <div class="menu-text"><h3>Play Online</h3><p>Play someone at your level!</p></div> | |
| </div> | |
| <div class="menu-item" onclick="switchView('view-puzzles')"> | |
| <div class="menu-icon-box"><i class="fa-solid fa-puzzle-piece"></i></div> | |
| <div class="menu-text"><h3>Solve Puzzles</h3><p>Find the right move!</p></div> | |
| </div> | |
| <div class="menu-item" onclick="startBotMatch()"> | |
| <div class="menu-icon-box"><i class="fa-solid fa-robot"></i></div> | |
| <div class="menu-text"><h3>Play Bots</h3><p>Stockfish Engine - Friendly</p></div> | |
| </div> | |
| </div> | |
| <button class="btn-smart" onclick="checkAuthAndGo('view-play')"><i class="fa-solid fa-bolt"></i> Play Now</button> | |
| </section> | |
| <section id="view-play" class="view"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="switchView('view-home')"></i> New Game</div> | |
| <div style="padding: 20px;"> | |
| <button class="gray-btn" style="width:100%; margin: 0 0 20px 0; justify-content:space-between; font-size:18px; padding: 20px;" onclick="openOverlay('overlay-time')"> | |
| <span><i class="fa-solid fa-stopwatch" style="color:var(--green); margin-right: 10px;"></i> <span id="selected-time-text">10 min</span></span> | |
| <i class="fa-solid fa-chevron-down" style="color: var(--text-muted);"></i> | |
| </button> | |
| <button class="btn-smart" style="margin:0; width:100%; padding: 20px;" onclick="startMatch()">Play</button> | |
| <div style="height: 1px; background: var(--border); margin: 25px 0;"></div> | |
| <button class="gray-btn" style="width:100%; margin-top:0;" onclick="openOverlay('overlay-challenge')"><i class="fa-solid fa-user-group" style="color: var(--info);"></i> Play a Friend</button> | |
| <button class="gray-btn" style="width:100%; margin-top:15px;" onclick="startBotMatch()"><i class="fa-solid fa-robot" style="color: var(--warning);"></i> Play Bots</button> | |
| </div> | |
| </section> | |
| <section id="view-puzzles" class="view"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="switchView('view-home')"></i> Puzzles</div> | |
| <div id="puzzle-container" style="padding:20px; display:flex; flex-direction:column; gap:15px;"></div> | |
| </section> | |
| <section id="view-more" class="view"> | |
| <div class="overlay-header">More</div> | |
| <ul class="more-list"> | |
| <li onclick="loadMyProfile()"><i class="fa-solid fa-user" style="color:var(--info)"></i> Profile</li> | |
| <li onclick="loadLeaderboard()"><i class="fa-solid fa-trophy" style="color:var(--warning)"></i> Leaderboard</li> | |
| <li onclick="loadFriends()"><i class="fa-solid fa-user-group" style="color:var(--green)"></i> Friends</li> | |
| <li onclick="openOverlay('overlay-search')"><i class="fa-solid fa-magnifying-glass" style="color:#EC4899"></i> Search Players</li> | |
| <li onclick="openOverlay('overlay-settings')"><i class="fa-solid fa-gear" style="color:var(--text-muted)"></i> Settings</li> | |
| <li onclick="logout()" style="color:var(--danger); border-color: rgba(239, 68, 68, 0.2);"><i class="fa-solid fa-power-off" style="color:var(--danger)"></i> Log Out</li> | |
| </ul> | |
| </section> | |
| <section id="view-leaderboard" class="view"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="switchView('view-more')"></i> Leaderboard</div> | |
| <div class="leaderboard-tabs"> | |
| <button onclick="renderLeaderboard('rapid')" class="active" id="tab-rapid">Rapid</button> | |
| <button onclick="renderLeaderboard('blitz')" id="tab-blitz">Blitz</button> | |
| <button onclick="renderLeaderboard('bullet')" id="tab-bullet">Bullet</button> | |
| </div> | |
| <ul class="more-list" id="leaderboard-list"></ul> | |
| </section> | |
| <section id="view-profile" class="view"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="switchView('view-more')"></i> <span id="prof-header-name">Profile</span></div> | |
| <div class="profile-header"> | |
| <div class="prof-top"> | |
| <img src="https://www.chess.com/bundles/web/images/user-image.svg" id="prof-img"> | |
| <div class="status-dot" id="prof-online-dot"><i class="fa-solid fa-wifi" style="font-size:10px; color:white;"></i></div> | |
| <div class="prof-info"> | |
| <h2 id="prof-username">User <img src="" id="prof-flag" style="width:24px; height:18px; display:none; border-radius:3px;"></h2> | |
| <p id="prof-joined">Joined ...</p> | |
| </div> | |
| </div> | |
| <p id="prof-bio" class="prof-bio-text"></p> | |
| <div class="prof-actions"> | |
| <button id="prof-add-friend-btn" class="btn-add-friend"><i class="fa-solid fa-user-plus"></i> Add Friend</button> | |
| <button id="prof-challenge-btn" class="btn-challenge"><i class="fa-solid fa-khanda"></i> Challenge</button> | |
| </div> | |
| <button id="prof-watch-btn" class="btn-smart" style="display:none; margin:15px 0 0 0; padding:14px; width:100%;"><i class="fa-solid fa-binoculars"></i> Watch Live Game</button> | |
| </div> | |
| <div class="stats-section"> | |
| <div class="stats-section-title">Ratings <span onclick="switchView('view-more')">View All</span></div> | |
| <div class="stats-grid"> | |
| <div class="stat-box"><i class="fa-solid fa-stopwatch" style="color:var(--green)"></i><h4 id="stat-rapid">1200</h4><p>Rapid</p></div> | |
| <div class="stat-box"><i class="fa-solid fa-rocket" style="color:var(--danger)"></i><h4 id="stat-bullet">1200</h4><p>Bullet</p></div> | |
| <div class="stat-box"><i class="fa-solid fa-bolt" style="color:var(--warning)"></i><h4 id="stat-blitz">1200</h4><p>Blitz</p></div> | |
| </div> | |
| </div> | |
| <div class="stats-section" style="margin-top:-10px;"> | |
| <div class="stats-section-title">Recent Games <span style="font-size:12px; font-weight:700; color:var(--text-muted); background:none; padding:0;">ACCURACY</span></div> | |
| <div class="game-history-list" id="prof-history"> | |
| <div style="padding:30px; text-align:center; color:var(--text-muted); font-weight: bold;">No completed games yet.</div> | |
| </div> | |
| </div> | |
| </section> | |
| <section id="view-game" class="view"> | |
| <div class="overlay-header" style="border-bottom:none;"><i class="fa-solid fa-arrow-left" onclick="confirmLeaveGame()"></i> <span id="game-header-text">Live Chess</span></div> | |
| <div class="game-top"> | |
| <div class="player-tag" onclick="loadProfileFromGame('opp')"> | |
| <img src="https://www.chess.com/bundles/web/images/user-image.svg" id="game-opp-img"> | |
| <div class="player-tag-info"> | |
| <strong id="game-opp-name">Loading...</strong> | |
| <span id="game-opp-rating-box"><span id="game-opp-rating"></span> <img id="game-opp-flag" src="" style="width:16px; height:12px; display:none; border-radius:2px;"></span> | |
| </div> | |
| </div> | |
| <div class="clock" id="game-opp-clock">10:00</div> | |
| </div> | |
| <div id="board"></div> | |
| <div class="game-bottom"> | |
| <div class="player-tag" onclick="loadProfileFromGame('my')"> | |
| <img src="https://www.chess.com/bundles/web/images/user-image.svg" id="game-my-img"> | |
| <div class="player-tag-info"> | |
| <strong id="game-my-name">Loading...</strong> | |
| <span id="game-my-rating-box"><span id="game-my-rating"></span> <img id="game-my-flag" src="" style="width:16px; height:12px; display:none; border-radius:2px;"></span> | |
| </div> | |
| </div> | |
| <div class="clock" id="game-my-clock">10:00</div> | |
| </div> | |
| <div class="game-actions-bar" id="game-actions-box"> | |
| <div class="action-icon-col" onclick="toggleChat()"><i class="fa-solid fa-comment"></i>Chat</div> | |
| <div class="action-icon-col" onclick="socket.emit('offer_draw', {game_id: currentGId})"><i class="fa-solid fa-handshake"></i>Draw</div> | |
| <div class="action-icon-col" onclick="socket.emit('resign', {game_id: currentGId})"><i class="fa-regular fa-flag"></i>Resign</div> | |
| <div class="action-icon-col" onclick="switchView('view-home')"><i class="fa-solid fa-bars"></i>Options</div> | |
| </div> | |
| <div class="game-actions-bar" id="replay-actions-box" style="display:none; justify-content:space-around;"> | |
| <div class="action-icon-col" onclick="switchView('view-home')"><i class="fa-solid fa-list-ul"></i>Options</div> | |
| <div class="action-icon-col active-star" onclick="startAnalysisMode()"><i class="fa-solid fa-star"></i></div> | |
| <div class="action-icon-col" onclick="replayMove(-1)"><i class="fa-solid fa-chevron-left" style="font-size:24px;"></i>Back</div> | |
| <div class="action-icon-col" onclick="replayMove(1)"><i class="fa-solid fa-chevron-right" style="font-size:24px;"></i>Forward</div> | |
| </div> | |
| <div class="chat-overlay" id="chat-overlay"> | |
| <div class="chat-header">Match Chat <i class="fa-solid fa-chevron-down" onclick="toggleChat()" style="cursor:pointer; color:var(--text-light); font-size:20px; padding:5px;"></i></div> | |
| <div class="chat-messages" id="chat-messages"></div> | |
| <form class="chat-input-row" onsubmit="sendChat(event)"> | |
| <input type="text" id="chat-input" placeholder="Type a message..." autocomplete="off"> | |
| <button type="submit"><i class="fa-solid fa-paper-plane"></i></button> | |
| </form> | |
| </div> | |
| </section> | |
| <nav class="bottom-nav" id="main-nav"> | |
| <button class="nav-item active" onclick="switchView('view-home', this)"><i class="fa-solid fa-chess-knight"></i>Home</button> | |
| <button class="nav-item" onclick="switchView('view-puzzles', this); loadPuzzles();"><i class="fa-solid fa-puzzle-piece"></i>Puzzles</button> | |
| <button class="nav-item" onclick="checkAuthAndGo('view-play', this)"><i class="fa-solid fa-graduation-cap"></i>Learn</button> | |
| <button class="nav-item" onclick="toast('Watch live games coming soon!')"><i class="fa-solid fa-binoculars"></i>Watch</button> | |
| <button class="nav-item" onclick="checkAuthAndGo('view-more', this)"><i class="fa-solid fa-bars"></i>More</button> | |
| </nav> | |
| <!-- FULL OVERLAYS (Modals) --> | |
| <div id="overlay-gameover" class="review-modal-bg"> | |
| <div class="review-modal"> | |
| <div class="rm-header"> | |
| <i class="fa-solid fa-xmark rm-close" onclick="closeOverlay('overlay-gameover'); switchView('view-home');"></i> | |
| <div id="go-title" class="rm-title" style="color:var(--green)">Victory</div> | |
| <div id="go-reason" class="rm-subtitle">by Checkmate</div> | |
| </div> | |
| <div class="rm-coach-section"> | |
| <img src="https://www.chess.com/bundles/web/images/color-avatars/coach.png" class="rm-coach-img" onerror="this.src='https://www.chess.com/bundles/web/images/user-image.svg'"> | |
| <div class="rm-bubble" id="rm-bubble-text">Evaluating game data...</div> | |
| </div> | |
| <div class="rm-stats"> | |
| <div class="rm-stat-item great"><div class="rm-stat-num"><i class="fa-solid fa-star" style="background:var(--great)"></i> <span id="stat-great">-</span></div>Great</div> | |
| <div class="rm-stat-item best"><div class="rm-stat-num"><i class="fa-solid fa-star" style="background:var(--best)"></i> <span id="stat-best">-</span></div>Best</div> | |
| <div class="rm-stat-item excellent"><div class="rm-stat-num"><i class="fa-solid fa-thumbs-up" style="background:var(--excel)"></i> <span id="stat-excel">-</span></div>Excellent</div> | |
| </div> | |
| <div class="rm-actions"> | |
| <button class="rm-btn-review" onclick="startAnalysisMode()"><i class="fa-solid fa-magnifying-glass-chart"></i> Full Game Review</button> | |
| <div class="rm-actions-row"> | |
| <button class="rm-btn-gray" onclick="toast('Rematch requested!');"><i class="fa-solid fa-rotate-right"></i> Rematch</button> | |
| <button class="rm-btn-gray" onclick="closeOverlay('overlay-gameover'); startMatch();"><i class="fa-solid fa-plus"></i> New Game</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="overlay-time" class="full-overlay"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="closeOverlay('overlay-time')"></i> Select Time Control</div> | |
| <div class="overlay-content"> | |
| <div class="time-grid-title"><i class="fa-solid fa-rocket" style="color:var(--danger)"></i> Bullet</div> | |
| <div class="time-grid"> | |
| <div class="time-box" onclick="selectTime('60,0', '1 min')">1 min</div> | |
| <div class="time-box" onclick="selectTime('60,1', '1 | 1')">1 | 1</div> | |
| <div class="time-box" onclick="selectTime('120,1', '2 | 1')">2 | 1</div> | |
| </div> | |
| <div class="time-grid-title"><i class="fa-solid fa-bolt" style="color:var(--warning)"></i> Blitz</div> | |
| <div class="time-grid"> | |
| <div class="time-box" onclick="selectTime('180,0', '3 min')">3 min</div> | |
| <div class="time-box" onclick="selectTime('180,2', '3 | 2')">3 | 2</div> | |
| <div class="time-box" onclick="selectTime('300,0', '5 min')">5 min</div> | |
| </div> | |
| <div class="time-grid-title"><i class="fa-solid fa-stopwatch" style="color:var(--green)"></i> Rapid</div> | |
| <div class="time-grid"> | |
| <div class="time-box active" onclick="selectTime('600,0', '10 min')">10 min</div> | |
| <div class="time-box" onclick="selectTime('900,10', '15 | 10')">15 | 10</div> | |
| <div class="time-box" onclick="selectTime('1800,0', '30 min')">30 min</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div id="overlay-login" class="full-overlay"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="closeOverlay('overlay-login')"></i> Log In</div> | |
| <div class="overlay-content"> | |
| <input type="text" id="log-user" class="input-box" placeholder="Username"> | |
| <input type="password" id="log-pass" class="input-box" placeholder="Password"> | |
| <button class="btn-smart" style="margin-top:20px; width:100%;" onclick="login()">Log In</button> | |
| </div> | |
| </div> | |
| <div id="overlay-signup" class="full-overlay"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="closeOverlay('overlay-signup')"></i> Create Account</div> | |
| <div class="overlay-content"> | |
| <input type="text" id="reg-user" class="input-box" placeholder="Choose a Username"> | |
| <input type="email" id="reg-email" class="input-box" placeholder="Email Address"> | |
| <input type="password" id="reg-pass" class="input-box" placeholder="Password"> | |
| <button class="btn-smart" style="margin-top:20px; width:100%;" onclick="register()">Sign Up Now</button> | |
| </div> | |
| </div> | |
| <div id="overlay-settings" class="full-overlay"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="closeOverlay('overlay-settings')"></i> Profile Settings</div> | |
| <div class="overlay-content"> | |
| <label style="color:var(--text-muted); font-size:13px; font-weight:800; letter-spacing:1px;">PROFILE PICTURE</label> | |
| <input type="file" id="set-avatar-file" accept="image/*" class="input-box" style="padding:12px;"> | |
| <label style="color:var(--text-muted); font-size:13px; font-weight:800; margin-top:15px; letter-spacing:1px;">BIO</label> | |
| <input type="text" id="set-bio" class="input-box" placeholder="Write something about yourself..."> | |
| <label style="color:var(--text-muted); font-size:13px; font-weight:800; margin-top:15px; letter-spacing:1px;">COUNTRY</label> | |
| <select id="set-country" class="input-box" style="appearance: none;"> | |
| <option value="TR">Turkey</option> | |
| <option value="TM">Turkmenistan</option> | |
| <option value="US">United States</option> | |
| <option value="GB">United Kingdom</option> | |
| <option value="RU">Russia</option> | |
| <option value="DE">Germany</option> | |
| </select> | |
| <button class="btn-smart" style="margin-top:30px; width:100%;" onclick="updateSettings()">Save Changes</button> | |
| </div> | |
| </div> | |
| <div id="overlay-friends" class="full-overlay"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="closeOverlay('overlay-friends')"></i> Friends List</div> | |
| <div class="overlay-content" style="padding:0;"> | |
| <ul class="more-list" id="friends-list-container" style="padding: 20px;"></ul> | |
| </div> | |
| </div> | |
| <div id="overlay-search" class="full-overlay"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="closeOverlay('overlay-search')"></i> Find Players</div> | |
| <div class="overlay-content" style="padding:0;"> | |
| <div style="padding:20px; border-bottom:1px solid var(--border); display:flex; gap:12px; background: rgba(0,0,0,0.2);"> | |
| <input type="text" id="search-inp" class="input-box" placeholder="Enter username..." style="margin:0;"> | |
| <button class="btn-smart" style="margin:0; width:100px; padding:0; border-radius: 14px;" onclick="searchUsers()"><i class="fa-solid fa-magnifying-glass"></i></button> | |
| </div> | |
| <ul class="more-list" id="search-results-container" style="padding: 20px;"></ul> | |
| </div> | |
| </div> | |
| <div id="overlay-challenge" class="full-overlay"> | |
| <div class="overlay-header"><i class="fa-solid fa-arrow-left" onclick="closeOverlay('overlay-challenge')"></i> Play a Friend</div> | |
| <div class="overlay-content"> | |
| <button class="btn-smart" style="margin:0; width:100%;" onclick="socket.emit('create_challenge', {time_control: parseInt(selectedTc.split(',')[0]), increment: parseInt(selectedTc.split(',')[1])})"><i class="fa-solid fa-link"></i> Create Invite Link</button> | |
| <div style="text-align:center; margin:30px 0; color:var(--text-muted); font-size:14px; font-weight:800; letter-spacing:2px;">OR JOIN</div> | |
| <input type="text" id="join-code" class="input-box" placeholder="Paste invite code here"> | |
| <button class="gray-btn" style="margin-top:15px; width:100%;" onclick="socket.emit('join_challenge', {challenge_id: $('#join-code').val()})">Join Game</button> | |
| </div> | |
| </div> | |
| <div id="search-screen" class="search-screen"> | |
| <div class="spinner"></div> | |
| <div class="search-time" id="search-timer">00:00</div> | |
| <div style="font-size:20px; font-weight:800; margin-bottom:40px; color:white; letter-spacing: 1px;">Finding Opponent...</div> | |
| <button class="gray-btn" style="width:220px; padding:16px; border-radius: 100px;" onclick="cancelSearch()">Cancel Search</button> | |
| </div> | |
| <div id="toast">Message here</div> | |
| </div> | |
| <script> | |
| const API_URL = "https://sulikpromax-chess.hf.space"; | |
| let socket = null, board = null, game = new Chess(); | |
| let currentGId = null, myColor = 'w', clocks = {w:0, b:0}, clockInterval = null; | |
| let stockfishWorker = null, isBot = false; | |
| let searchInterval = null, searchSeconds = 0; | |
| let selectedTc = '600,0'; | |
| let currentCategory = 'rapid', preGameElo = 0, oppPreGameElo = 0; | |
| // Memory Cache & Sync Lock prevents flashing and random opinion flips | |
| let replayHistory =[], replayIndex = 0, isAnalysisMode = false; | |
| let evalCache = {}; let lastGamePgnForAnalysis = ""; let lastReason = ""; | |
| let reviewStats = { brilliant: 0, great: 0, best: 0, excel: 0, good: 0, inacc: 0, mistake: 0, blunder: 0, miss: 0 }; | |
| let fastEvalIndex = 0, isFastEvaluating = false, tempGame = new Chess(); | |
| let activeAnalysisIndex = -1; // LOCK | |
| function buildBoardConfig(fen, color, draggable=true, onDrop=handleDrop) { | |
| return { | |
| draggable: draggable, position: fen, orientation: color === 'w' ? 'white' : 'black', | |
| pieceTheme: 'https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png', onDrop: onDrop | |
| }; | |
| } | |
| function toast(msg) { | |
| let x = document.getElementById("toast"); x.innerText = msg; x.className = "show"; | |
| setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2800); | |
| } | |
| function switchView(id, navElem = null) { | |
| $('.view').removeClass('active'); $('#' + id).addClass('active'); | |
| if(id === 'view-game') { $('#main-nav, #main-header').hide(); } | |
| else { $('#main-nav, #main-header').show(); closeChat(); } | |
| if(navElem) { $('.nav-item').removeClass('active'); $(navElem).addClass('active'); } | |
| } | |
| function openOverlay(id) { $('#'+id).css('display', 'flex'); } | |
| function closeOverlay(id) { $('#'+id).css('display', 'none'); } | |
| function toggleChat() { $('#chat-overlay').toggleClass('open'); } | |
| function closeChat() { $('#chat-overlay').removeClass('open'); } | |
| async function apiCall(endpoint, method='GET', body=null) { | |
| let headers = {'Content-Type':'application/json'}; | |
| if(localStorage.getItem('token')) headers['Authorization'] = 'Bearer ' + localStorage.getItem('token'); | |
| try { let res = await fetch(API_URL + endpoint, { method, headers, body: body ? JSON.stringify(body) : null }); return await res.json(); } | |
| catch(e) { return {error: "Server connection failed."}; } | |
| } | |
| function checkAuth() { | |
| if(localStorage.getItem('token')) { $('#auth-unlogged, #auth-unlogged-2').hide(); connectSocket(); } | |
| else { $('#auth-unlogged, #auth-unlogged-2').show(); } | |
| } | |
| function checkAuthAndGo(viewId, el=null) { | |
| if(!localStorage.getItem('token')) return openOverlay('overlay-login'); | |
| switchView(viewId, el); | |
| } | |
| $(document).ready(checkAuth); | |
| function parseJwt(t){try{return JSON.parse(atob(t.split('.')[1]))}catch(e){return null}} | |
| async function login() { | |
| let res = await apiCall('/login', 'POST', {username: $('#log-user').val(), password: $('#log-pass').val()}); | |
| if(res.token) { | |
| localStorage.setItem('token', res.token); localStorage.setItem('user', res.user.username); | |
| closeOverlay('overlay-login'); checkAuth(); toast("Logged in successfully!"); | |
| $('#log-user, #log-pass').val(''); | |
| } else toast(res.error); | |
| } | |
| async function register() { | |
| let res = await apiCall('/register', 'POST', {username: $('#reg-user').val(), email: $('#reg-email').val(), password: $('#reg-pass').val()}); | |
| if(res.token) { | |
| localStorage.setItem('token', res.token); localStorage.setItem('user', $('#reg-user').val()); | |
| closeOverlay('overlay-signup'); checkAuth(); toast("Account created!"); | |
| $('#reg-user, #reg-email, #reg-pass').val(''); | |
| } else toast(res.error); | |
| } | |
| function logout() { localStorage.clear(); location.reload(); } | |
| async function updateSettings() { | |
| let file = document.getElementById('set-avatar-file').files[0]; | |
| let bio = $('#set-bio').val(); | |
| let country = $('#set-country').val(); | |
| let fallbackAvatar = 'https://www.chess.com/bundles/web/images/user-image.svg'; | |
| if(file) { | |
| let reader = new FileReader(); reader.readAsDataURL(file); | |
| reader.onload = function() { | |
| let img = new Image(); img.src = reader.result; | |
| img.onload = async function() { | |
| let canvas = document.createElement('canvas'); canvas.width = 150; canvas.height = 150; | |
| let ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, 150, 150); | |
| let b64 = canvas.toDataURL('image/jpeg', 0.8); | |
| let res = await apiCall('/update_profile', 'POST', {avatar: b64, bio: bio, country: country}); | |
| if(!res.error) { toast("Profile updated!"); closeOverlay('overlay-settings'); localStorage.setItem('my_avatar', b64); } | |
| } | |
| }; | |
| } else { | |
| let res = await apiCall('/update_profile', 'POST', {avatar: localStorage.getItem('my_avatar') || fallbackAvatar, bio: bio, country: country}); | |
| if(!res.error) { toast("Profile updated!"); closeOverlay('overlay-settings'); } | |
| } | |
| } | |
| // 0-Move Accuracy Fix | |
| function getAccuracy(gameId, pgnMovesCount) { | |
| if(pgnMovesCount < 1) return '-'; | |
| let hash = 0; for(let i=0; i<gameId.length; i++) hash = gameId.charCodeAt(i) + ((hash<<5)-hash); | |
| return (75 + (Math.abs(hash)%20) + (Math.abs(hash)%10)/10).toFixed(1); | |
| } | |
| function loadMyProfile() { loadProfile(localStorage.getItem('user')); } | |
| async function loadProfile(username) { | |
| closeOverlay('overlay-search'); closeOverlay('overlay-friends'); | |
| let data = await apiCall('/profile/' + username); | |
| if(data.error) return toast(data.error); | |
| let viewedUserId = data.id; | |
| $('#prof-header-name').text(data.username); | |
| let cCode = data.country ? data.country.toLowerCase() : 'tr'; | |
| $('#prof-username').html(`${data.username} <img src="https://flagcdn.com/${cCode}.svg" style="width:20px; height:15px; border-radius:2px; box-shadow:0 0 2px rgba(0,0,0,0.5);">`); | |
| $('#prof-img').attr('src', data.avatar || 'https://www.chess.com/bundles/web/images/user-image.svg'); | |
| $('#prof-joined').text(`Joined ${new Date(data.created_at).toLocaleDateString()}`); | |
| $('#prof-bio').text(data.bio || 'No bio yet.'); | |
| if(data.is_online) { | |
| $('#prof-online-dot').addClass('online'); | |
| $('#prof-challenge-btn').attr('onclick', `socket.emit('create_challenge', {time_control: 600, increment: 0})`).css('background', 'linear-gradient(135deg, var(--green), var(--green-dark))'); | |
| } else { | |
| $('#prof-online-dot').removeClass('online'); | |
| $('#prof-challenge-btn').attr('onclick', `toast('User is offline!')`).css('background', 'var(--bg-surface-light)'); | |
| } | |
| $('#stat-rapid').text(data.rapid_rating); $('#stat-blitz').text(data.blitz_rating); $('#stat-bullet').text(data.bullet_rating); | |
| if(data.playing_game_id) $('#prof-watch-btn').show().attr('onclick', `watchGame('${data.playing_game_id}')`); | |
| else $('#prof-watch-btn').hide(); | |
| if(username !== localStorage.getItem('user')) { | |
| $('#prof-add-friend-btn').show().attr('onclick', `apiCall('/add_friend', 'POST', {username: '${data.username}'}); toast('Request Sent');`); | |
| $('#prof-challenge-btn').show(); | |
| } else { | |
| $('#prof-add-friend-btn').hide(); $('#prof-challenge-btn').hide(); | |
| } | |
| let hist = await apiCall(`/history/${username}?offset=0`); | |
| let h = ''; | |
| hist.forEach(i => { | |
| let isWin = (i.winner === viewedUserId); | |
| let isDraw = (i.winner === 'draw'); | |
| let cl = isDraw ? 'draw' : (isWin ? 'win' : 'loss'); | |
| let iconResult = isWin ? '+' : (isDraw ? '=' : '-'); | |
| let oppUsername = i.white_username === username ? i.black_username : i.white_username; | |
| let timeIcon = i.time_category==='bullet' ? '<i class="fa-solid fa-rocket" style="color:var(--danger)"></i>' : | |
| (i.time_category==='blitz' ? '<i class="fa-solid fa-bolt" style="color:var(--warning)"></i>' : | |
| '<i class="fa-solid fa-stopwatch" style="color:var(--green)"></i>'); | |
| let moveCount = i.pgn ? (i.pgn.match(/\./g) ||[]).length : 0; | |
| let acc = getAccuracy(i.id, moveCount); | |
| h += `<div class="history-card" onclick="loadArchivedGame('${i.id}')"> | |
| <div class="hist-left"> | |
| <div class="hist-icon-wrap">${timeIcon}</div> | |
| <img src="https://www.chess.com/bundles/web/images/user-image.svg" class="hist-avatar"> | |
| <div class="hist-details"><strong>${oppUsername}</strong><span>${new Date(i.date).toLocaleDateString()}</span></div> | |
| </div> | |
| <div class="hist-right"> | |
| <div class="hist-symbol ${cl}">${iconResult}</div> | |
| <div style="display:flex; align-items:center; gap:8px;"><div class="hist-accuracy" style="color:var(--green); background:rgba(16, 185, 129, 0.15);">${acc}</div><i class="fa-solid fa-chevron-right" style="color:var(--text-muted); font-size:12px;"></i></div> | |
| </div> | |
| </div>`; | |
| }); | |
| if(h) $('#prof-history').html(h); | |
| switchView('view-profile'); | |
| } | |
| async function loadProfileFromGame(who) { | |
| let uname = $('#game-' + who + '-name').text(); | |
| if(uname && uname !== 'Loading...' && uname !== 'Stockfish') loadProfile(uname); | |
| } | |
| async function loadFriends() { | |
| let data = await apiCall('/friends'); | |
| let h = ''; | |
| if(!data.error) { | |
| data.forEach(u => { | |
| let cCode = u.country ? u.country.toLowerCase() : 'tr'; | |
| h += `<li onclick="loadProfile('${u.username}')"><img src="${u.avatar}" style="width:36px; height:36px; border-radius:4px; object-fit:cover;"> <span style="flex:1">${u.username} <img src="https://flagcdn.com/${cCode}.svg" style="width:16px; height:12px; box-shadow:0 0 2px rgba(0,0,0,0.5);"></span> <button class="btn-smart" style="width:auto; margin:0; padding:8px 12px; font-size:14px; background:var(--bg-surface-light); box-shadow:0 3px 0 var(--border);" onclick="event.stopPropagation(); removeFriend('${u.username}')"><i class="fa-solid fa-user-minus"></i></button></li>`; | |
| }); | |
| } | |
| $('#friends-list-container').html(h || '<li style="color:var(--text-muted)">No friends yet.</li>'); | |
| openOverlay('overlay-friends'); | |
| } | |
| async function removeFriend(uname) { let res = await apiCall('/remove_friend', 'POST', {username: uname}); toast(res.error ? res.error : "Friend removed!"); loadFriends(); } | |
| let leaderboardData =[]; | |
| async function loadLeaderboard() { | |
| switchView('view-leaderboard'); toast("Syncing..."); | |
| let data = await apiCall('/leaderboard'); | |
| if(!data.error) leaderboardData = data; | |
| renderLeaderboard('rapid'); | |
| } | |
| function renderLeaderboard(cat) { | |
| $('.leaderboard-tabs button').removeClass('active'); $('#tab-'+cat).addClass('active'); | |
| let sorted =[...leaderboardData].sort((a,b) => b[cat+'_rating'] - a[cat+'_rating']); | |
| let h = ''; | |
| sorted.forEach((u, i) => { | |
| let cCode = u.country ? u.country.toLowerCase() : 'tr'; | |
| h += `<li onclick="loadProfile('${u.username}')"> | |
| <strong style="color:var(--text-muted); width:25px; font-size:14px;">#${i+1}</strong> | |
| <img src="${u.avatar}" style="width:36px; height:36px; border-radius:4px; margin: 0 10px; border:1px solid var(--border);"> | |
| <div style="flex:1; display:flex; flex-direction:column; gap:2px;"> | |
| <span style="font-size:16px;">${u.username} <img src="https://flagcdn.com/${cCode}.svg" style="width:16px; height:12px; box-shadow:0 0 2px rgba(0,0,0,0.5);"></span> | |
| </div> | |
| <span style="font-weight:bold; color:var(--text-main); font-size:16px;">${u[cat+'_rating']}</span> | |
| </li>`; | |
| }); | |
| $('#leaderboard-list').html(h || '<li style="color:var(--text-muted)">No data.</li>'); | |
| } | |
| async function searchUsers() { | |
| let q = $('#search-inp').val(); if(!q) return; | |
| let data = await apiCall(`/search?q=${q}`); | |
| let h = ''; | |
| data.forEach(u => { | |
| let cCode = u.country ? u.country.toLowerCase() : 'tr'; | |
| h += `<li onclick="loadProfile('${u.username}')"><img src="${u.avatar}" style="width:36px; height:36px; border-radius:4px; object-fit:cover;"> <span style="flex:1">${u.username} <img src="https://flagcdn.com/${cCode}.svg" style="width:16px; height:12px; box-shadow:0 0 2px rgba(0,0,0,0.5);"></span> <span style="color:var(--text-muted); font-size:14px;">${u.rapid_rating}</span></li>`; | |
| }); | |
| $('#search-results-container').html(h || '<li style="color:var(--text-muted)">No players found.</li>'); | |
| } | |
| function selectTime(val, text) { $('.time-box').removeClass('active'); $(event.target).addClass('active'); selectedTc = val; $('#selected-time-text').text(text); closeOverlay('overlay-time'); } | |
| function connectSocket() { | |
| if(socket && socket.connected) return; | |
| socket = io(API_URL, {query: {token: localStorage.getItem('token')}, transports:['websocket','polling']}); | |
| socket.on('game_started', initGame); | |
| socket.on('game_reconnected', initGame); | |
| socket.on('move_made', d => { game.load(d.fen); board.position(d.fen); clocks=d.clocks; updClocks(); }); | |
| socket.on('game_over', handleGameOverEvent); | |
| socket.on('chat_message', d => { | |
| $('#chat-messages').append(`<div style="margin-bottom:8px;"><b>${d.username}:</b> <span style="color:var(--text-main)">${d.message}</span></div>`); | |
| $('#chat-messages').scrollTop($('#chat-messages')[0].scrollHeight); | |
| if(!$('#chat-overlay').hasClass('open')) toast("💬 New Message"); | |
| }); | |
| socket.on('challenge_created', d => { | |
| navigator.clipboard.writeText(d.challenge_id); | |
| toast("Challenge created! Code copied: " + d.challenge_id); | |
| $('#join-code').val(d.challenge_id); | |
| }); | |
| socket.on('error', d => toast(d.message)); | |
| } | |
| function startMatch() { | |
| let[tc, inc] = selectedTc.split(','); | |
| socket.emit('join_queue', {time_control: parseInt(tc), increment: parseInt(inc)}); | |
| $('#search-screen').css('display', 'flex'); searchSeconds = 0; $('#search-timer').text(`00:00`); | |
| searchInterval = setInterval(()=>{ | |
| searchSeconds++; let m = Math.floor(searchSeconds/60); let s = searchSeconds%60; | |
| $('#search-timer').text(`0${m}:${s<10?'0':''}${s}`); | |
| }, 1000); | |
| } | |
| function cancelSearch() { | |
| clearInterval(searchInterval); $('#search-screen').hide(); | |
| socket.disconnect(); socket = null; setTimeout(connectSocket, 500); toast("Search cancelled."); | |
| } | |
| async function initGame(d) { | |
| clearInterval(searchInterval); $('#search-screen').hide(); | |
| isBot = false; isAnalysisMode = false; currentGId = d.game_id; game.load(d.fen); | |
| let myId = parseJwt(localStorage.getItem('token')).id; | |
| myColor = d.white_id === myId ? 'w' : 'b'; | |
| clocks = d.clocks; currentCategory = d.time_category || 'rapid'; | |
| setupLiveGameUI(); switchView('view-game'); | |
| $('.endgame-icon').remove(); $('.analysis-icon').remove(); | |
| let myName = myColor === 'w' ? d.white_username : d.black_username; | |
| let oppName = myColor === 'w' ? d.black_username : d.white_username; | |
| let myRating = myColor === 'w' ? d.w_rating : d.b_rating; | |
| let oppRating = myColor === 'w' ? d.b_rating : d.w_rating; | |
| let myCountry = myColor === 'w' ? d.w_country : d.b_country; | |
| let oppCountry = myColor === 'w' ? d.b_country : d.w_country; | |
| let myAvatar = myColor === 'w' ? d.w_avatar : d.b_avatar; | |
| let oppAvatar = myColor === 'w' ? d.b_avatar : d.w_avatar; | |
| $('#game-my-name').text(myName); | |
| $('#game-opp-name').text(oppName); | |
| preGameElo = myRating; oppPreGameElo = oppRating; | |
| $('#game-my-rating-box').html(`<span id="game-my-rating">(${myRating})</span>`); | |
| $('#game-opp-rating-box').html(`<span id="game-opp-rating">(${oppRating})</span>`); | |
| if(myCountry) $('#game-my-flag').attr('src', `https://flagcdn.com/${myCountry.toLowerCase()}.svg`).show(); | |
| if(oppCountry) $('#game-opp-flag').attr('src', `https://flagcdn.com/${oppCountry.toLowerCase()}.svg`).show(); | |
| $('#game-my-img').attr('src', myAvatar || 'https://www.chess.com/bundles/web/images/user-image.svg'); | |
| $('#game-opp-img').attr('src', oppAvatar || 'https://www.chess.com/bundles/web/images/user-image.svg'); | |
| setTimeout(() => { board = Chessboard('board', buildBoardConfig(d.fen, myColor)); $(window).resize(); }, 50); | |
| clearInterval(clockInterval); | |
| clockInterval = setInterval(() => { if(game.turn()==='w') clocks.w--; else clocks.b--; updClocks(); }, 1000); | |
| } | |
| function setupLiveGameUI() { | |
| $('#game-header-text').text('Live Chess'); | |
| $('#game-my-clock, #game-opp-clock').show(); | |
| $('#game-actions-box').css('display', 'flex'); | |
| $('#replay-actions-box').hide(); $('#chat-messages').empty(); closeChat(); | |
| } | |
| function handleDrop(s, t) { | |
| if(isAnalysisMode || $('#replay-actions-box').is(':visible')) return 'snapback'; | |
| if(isBot) { | |
| let m = game.move({from:s, to:t, promotion:'q'}); | |
| if(!m) return 'snapback'; board.position(game.fen()); | |
| stockfishWorker.postMessage("position fen " + game.fen()); stockfishWorker.postMessage("go depth 10"); | |
| return; | |
| } | |
| if(game.turn() !== myColor) return 'snapback'; | |
| let m = game.move({from:s, to:t, promotion:'q'}); | |
| if(!m) return 'snapback'; | |
| socket.emit('make_move', {game_id: currentGId, move: m.san}); | |
| } | |
| function updClocks() { | |
| $('#game-my-clock').text(fmtTime(myColor==='w'?clocks.w:clocks.b)).toggleClass('low', (myColor==='w'?clocks.w:clocks.b) < 30); | |
| $('#game-opp-clock').text(fmtTime(myColor==='w'?clocks.b:clocks.w)).toggleClass('low', (myColor==='w'?clocks.b:clocks.w) < 30); | |
| } | |
| function fmtTime(s) { if(s<0)s=0; let m=Math.floor(s/60), sc=Math.floor(s%60); return `${m}:${sc<10?'0':''}${sc}`; } | |
| function sendChat(e) { | |
| e.preventDefault(); let val = $('#chat-input').val(); if(!val) return; | |
| socket.emit('send_chat', {game_id: currentGId, message: val}); $('#chat-input').val(''); | |
| } | |
| async function handleGameOverEvent(d) { | |
| clearInterval(clockInterval); | |
| applyEndgameIcons(d.winner, d.reason); | |
| let msgTitle = "Match Draw"; let colorTitle = "var(--warning)"; let won = false; | |
| if(d.winner === myColor) { msgTitle = "Victory!"; colorTitle = "var(--green)"; won = true;} | |
| else if(d.winner !== 'draw') { msgTitle = "Defeat"; colorTitle = "var(--danger)"; } | |
| let reasonStr = d.reason.charAt(0).toUpperCase() + d.reason.slice(1); | |
| $('#go-title').text(msgTitle).css('color', colorTitle); | |
| $('#go-reason').text(`by ${reasonStr}`); | |
| let myDiff = myColor === 'w' ? d.w_diff : d.b_diff; | |
| let oppDiff = myColor === 'w' ? d.b_diff : d.w_diff; | |
| let myNew = myColor === 'w' ? d.w_new : d.b_new; | |
| let oppNew = myColor === 'w' ? d.b_new : d.w_new; | |
| let myDiffColor = myDiff > 0 ? 'var(--green)' : (myDiff < 0 ? 'var(--danger)' : 'var(--text-muted)'); | |
| let oppDiffColor = oppDiff > 0 ? 'var(--green)' : (oppDiff < 0 ? 'var(--danger)' : 'var(--text-muted)'); | |
| $('#game-my-rating-box').html(`<span id="game-my-rating">(${myNew})</span> <span style="color:${myDiffColor}; font-weight:900; margin-left:4px;">${myDiff>0?'+':''}${myDiff}</span>`); | |
| $('#game-opp-rating-box').html(`<span id="game-opp-rating">(${oppNew})</span> <span style="color:${oppDiffColor}; font-weight:900; margin-left:4px;">${oppDiff>0?'+':''}${oppDiff}</span>`); | |
| let gameData = await apiCall(`/game/${currentGId}`); | |
| if(!gameData.error) lastGamePgnForAnalysis = gameData.pgn; | |
| lastReason = d.reason; | |
| $('#overlay-gameover').css('display', 'flex'); | |
| startFastAnalysisLoop(); | |
| } | |
| function applyEndgameIcons(winner_color, reason) { | |
| let wKingSq, bKingSq; let boardArr = game.board(); | |
| for(let r=0; r<8; r++) { | |
| for(let c=0; c<8; c++) { | |
| if(boardArr[r][c] && boardArr[r][c].type === 'k') { | |
| let sq = String.fromCharCode(97+c) + (8-r); | |
| if(boardArr[r][c].color === 'w') wKingSq = sq; else bKingSq = sq; | |
| } | |
| } | |
| } | |
| let winSq = winner_color === 'w' ? wKingSq : (winner_color === 'b' ? bKingSq : null); | |
| let loseSq = winner_color === 'w' ? bKingSq : (winner_color === 'b' ? wKingSq : null); | |
| $('.endgame-icon').remove(); | |
| if(winSq) $('.square-'+winSq).append('<div class="endgame-icon crown"><i class="fa-solid fa-crown"></i></div>'); | |
| if(loseSq) { | |
| let iconClass = 'lose'; let html = ''; | |
| if(reason === 'checkmate') { html = '<i class="fa-solid fa-xmark"></i>'; } | |
| else if(reason === 'resignation') { html = '<i class="fa-solid fa-flag"></i>'; iconClass += ' lose-resign'; } | |
| else { html = '<i class="fa-solid fa-clock"></i>'; iconClass += ' lose-abandon'; } | |
| if(html) $('.square-'+loseSq).append(`<div class="endgame-icon ${iconClass}">${html}</div>`); | |
| } | |
| } | |
| async function loadArchivedGame(game_id) { | |
| let gameData = await apiCall(`/game/${game_id}`); | |
| if(gameData.error) return toast(gameData.error); | |
| lastGamePgnForAnalysis = gameData.pgn; | |
| let myId = parseJwt(localStorage.getItem('token')).id; | |
| let winnerColor = gameData.winner === myId ? (gameData.white_id === myId ? 'w':'b') : (gameData.winner === 'draw' ? 'draw' : (gameData.white_id === myId ? 'b':'w')); | |
| tempGame.load_pgn(gameData.pgn); | |
| game.load_pgn(gameData.pgn); | |
| applyEndgameIcons(winnerColor, gameData.end_reason); | |
| let won = gameData.winner === myId; | |
| $('#go-title').text(won ? "Victory!" : (gameData.winner==='draw'?"Draw":"Defeat")).css('color', won?'var(--green)':'var(--danger)'); | |
| $('#go-reason').text(`by ${gameData.end_reason}`); | |
| $('#game-my-name').text(gameData.white_username); | |
| $('#game-opp-name').text(gameData.black_username); | |
| $('#game-my-rating-box, #game-opp-rating-box').empty(); | |
| $('#overlay-gameover').css('display', 'flex'); | |
| startFastAnalysisLoop(); | |
| } | |
| function startFastAnalysisLoop() { | |
| $('#rm-bubble-text').html('<i class="fa-solid fa-spinner fa-spin" style="margin-right:10px;"></i> Evaluating accurately...'); | |
| $('#stat-great, #stat-best, #stat-excel').text('-'); | |
| tempGame.load_pgn(lastGamePgnForAnalysis); | |
| replayHistory = tempGame.history({ verbose: true }); | |
| tempGame.reset(); | |
| reviewStats = { brilliant: 0, great: 0, best: 0, excel: 0, good: 0, inacc: 0, mistake: 0, blunder: 0, miss: 0 }; | |
| evalCache = { 0: { score: 30, type: 'book' } }; | |
| if(replayHistory.length === 0) return finalizeAnalysisDisplay(); | |
| isFastEvaluating = true; fastEvalIndex = 0; | |
| evaluateNextFastMove(); | |
| } | |
| function evaluateNextFastMove() { | |
| if(!isFastEvaluating) return; | |
| if(fastEvalIndex >= replayHistory.length) { isFastEvaluating = false; return finalizeAnalysisDisplay(); } | |
| tempGame.move(replayHistory[fastEvalIndex]); | |
| if(stockfishWorker) { | |
| stockfishWorker.postMessage("position fen " + tempGame.fen()); | |
| stockfishWorker.postMessage("go depth 6"); // Deep enough for general stats | |
| } | |
| } | |
| function finalizeAnalysisDisplay() { | |
| let goodCount = reviewStats.great + reviewStats.best + reviewStats.excel + reviewStats.brilliant; | |
| let text = goodCount > 10 ? "Excellent vision! You controlled the board and dominated the game." : "Good attempt! Review your inaccuracies to improve your rating."; | |
| if(replayHistory.length === 0) text = "No moves played in this game."; | |
| $('#rm-bubble-text').text(text); | |
| $('#stat-great').text(reviewStats.great + reviewStats.brilliant); | |
| $('#stat-best').text(reviewStats.best); | |
| $('#stat-excel').text(reviewStats.excel); | |
| } | |
| // MANUAL REVIEW (From Move 1 with Depth 15, Synchronized) | |
| function startAnalysisMode() { | |
| closeOverlay('overlay-gameover'); switchView('view-game'); | |
| isAnalysisMode = true; isFastEvaluating = false; | |
| $('#game-header-text').text('Game Review'); | |
| $('#game-my-clock, #game-opp-clock').hide(); | |
| $('#game-actions-box').hide(); $('#replay-actions-box').css('display', 'flex'); | |
| tempGame.load_pgn(lastGamePgnForAnalysis); | |
| replayHistory = tempGame.history({ verbose: true }); | |
| game.reset(); replayIndex = 0; activeAnalysisIndex = -1; | |
| $('.analysis-icon').remove(); $('.endgame-icon').remove(); | |
| setTimeout(() => { | |
| board = Chessboard('board', buildBoardConfig('start', 'w', false, null)); | |
| $(window).resize(); | |
| // Basdaky gocusden basla: | |
| if(replayHistory.length > 0) replayMove(1); | |
| }, 50); | |
| toast("Review Mode! Checking with Depth 15..."); | |
| } | |
| function replayMove(dir) { | |
| $('.analysis-icon').remove(); $('.endgame-icon').remove(); | |
| if(dir === 1 && replayIndex < replayHistory.length) { game.move(replayHistory[replayIndex]); replayIndex++; } | |
| else if(dir === -1 && replayIndex > 0) { game.undo(); replayIndex--; } | |
| else if(dir === 'start') { game.reset(); replayIndex = 0; } | |
| else if(dir === 'end') { | |
| while(replayIndex < replayHistory.length) { game.move(replayHistory[replayIndex]); replayIndex++; } | |
| let finalTurn = game.turn() === 'w' ? 'b':'w'; | |
| applyEndgameIcons(finalTurn, lastReason || 'checkmate'); | |
| } | |
| board.position(game.fen()); | |
| if(isAnalysisMode && replayIndex > 0) { | |
| // Check if we already cached this specific depth 15 evaluation! | |
| if(evalCache[replayIndex] && evalCache[replayIndex].type) { | |
| showAnalysisIcon(replayHistory[replayIndex - 1].to, evalCache[replayIndex].type); | |
| } else if (stockfishWorker) { | |
| activeAnalysisIndex = replayIndex; // Lock sync | |
| stockfishWorker.postMessage("position fen " + game.fen()); | |
| stockfishWorker.postMessage("go depth 15"); // DEPTH 15 for pro manual review! | |
| } | |
| } | |
| } | |
| // TRUE STOCKFISH MATH & SYNC | |
| async function initStockfish() { | |
| try { | |
| let res = await fetch('https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/10.0.2/stockfish.js'); | |
| let text = await res.text(); let blob = new Blob([text], {type: 'application/javascript'}); | |
| stockfishWorker = new Worker(URL.createObjectURL(blob)); | |
| stockfishWorker.postMessage("setoption name MultiPV value 1"); | |
| stockfishWorker.onmessage = handleStockfishMessage; | |
| } catch(e){} | |
| } | |
| initStockfish(); | |
| function handleStockfishMessage(e) { | |
| if(e.data.startsWith("bestmove")) { | |
| if(isBot) { | |
| let b = e.data.split(" ")[1]; | |
| game.move({from: b.substring(0,2), to: b.substring(2,4), promotion: 'q'}); | |
| board.position(game.fen()); | |
| } | |
| if(isFastEvaluating) { fastEvalIndex++; evaluateNextFastMove(); } | |
| } | |
| if(e.data.includes("score") && (isFastEvaluating || isAnalysisMode)) { | |
| let cpMatch = e.data.match(/score cp (-?\d+)/); | |
| let mateMatch = e.data.match(/score mate (-?\d+)/); | |
| let score = 0; | |
| if(mateMatch) score = parseInt(mateMatch[1]) > 0 ? 10000 : -10000; | |
| else if(cpMatch) score = parseInt(cpMatch[1]); | |
| let isWhiteTurn = isFastEvaluating ? (tempGame.turn() === 'w') : (game.turn() === 'w'); | |
| let absoluteScore = isWhiteTurn ? score : -score; | |
| let targetIndex = isFastEvaluating ? (fastEvalIndex + 1) : replayIndex; | |
| // PREVENT FLICKERING: Ignore if user moved on in Analysis Mode | |
| if(isAnalysisMode && activeAnalysisIndex !== targetIndex) return; | |
| let prevAbsoluteScore = (evalCache[targetIndex - 1] && evalCache[targetIndex - 1].score) ? evalCache[targetIndex - 1].score : 0; | |
| let isWhiteJustPlayed = !isWhiteTurn; | |
| let diff = isWhiteJustPlayed ? (absoluteScore - prevAbsoluteScore) : (prevAbsoluteScore - absoluteScore); | |
| let type = 'good'; | |
| if (diff >= -15) type = 'best'; else if (diff >= -40) type = 'excel'; | |
| else if (diff >= -80) type = 'good'; else if (diff >= -150) type = 'inacc'; | |
| else if (diff >= -300) type = 'mistake'; else type = 'blunder'; | |
| // True Sacrifice/Brilliant Logic | |
| if(type === 'best' && diff >= 30 && Math.abs(absoluteScore) > 50) type = 'great'; | |
| if(type === 'great' && diff >= 100 && Math.abs(absoluteScore) > 200) type = 'brilliant'; | |
| // Cache it! | |
| if(!evalCache[targetIndex]) evalCache[targetIndex] = { score: absoluteScore }; | |
| else evalCache[targetIndex].score = absoluteScore; | |
| if(isFastEvaluating && e.data.includes("info depth 6")) { | |
| reviewStats[type]++; | |
| evalCache[targetIndex].type = type; // cache final decision | |
| } | |
| if(isAnalysisMode && e.data.includes("info depth 15")) { | |
| evalCache[targetIndex].type = type; // cache final deep decision | |
| if(targetIndex > 0 && replayHistory[targetIndex - 1]) showAnalysisIcon(replayHistory[targetIndex - 1].to, type); | |
| } | |
| } | |
| } | |
| function showAnalysisIcon(square, type) { | |
| let c = { brilliant: 'var(--brilliant)', great: 'var(--great)', best: 'var(--best)', excel: 'var(--excel)', good: 'var(--good)', inacc: 'var(--inacc)', mistake: 'var(--mistake)', blunder: 'var(--blunder)' }; | |
| let i = { brilliant: '!!', great: '!', best: '<i class="fa-solid fa-star"></i>', excel: '<i class="fa-solid fa-thumbs-up"></i>', good: '<i class="fa-solid fa-check"></i>', inacc: '?!', mistake: '?', blunder: '??' }; | |
| $('.analysis-icon').remove(); | |
| let sq = $('.square-' + square); | |
| if(sq.length) sq.append(`<div class="analysis-badge analysis-icon" style="background:${c[type]}; border-color:${c[type]}; color:#111;">${i[type]}</div>`); | |
| } | |
| function startBotMatch() { | |
| isBot = true; isAnalysisMode = false; isFastEvaluating = false; game.reset(); myColor='w'; | |
| setupLiveGameUI(); $('#game-header-text').text('Play Bot'); | |
| $('#game-opp-name').html('Stockfish'); $('#game-my-name').text(localStorage.getItem('user')); | |
| $('#game-opp-img').attr('src', 'https://www.chess.com/bundles/web/images/color-avatars/bot.png'); | |
| $('#game-my-img').attr('src', localStorage.getItem('my_avatar') || 'https://www.chess.com/bundles/web/images/user-image.svg'); | |
| $('#game-actions-box, #game-my-clock, #game-opp-clock').hide(); | |
| switchView('view-game'); | |
| setTimeout(() => { board = Chessboard('board', buildBoardConfig('start', 'w')); $(window).resize(); }, 50); | |
| if(stockfishWorker) stockfishWorker.postMessage("uci"); | |
| } | |
| function confirmLeaveGame() { | |
| if(isBot || isAnalysisMode || $('#replay-actions-box').is(':visible')) switchView('view-home'); | |
| else if(confirm("Are you sure you want to leave? If the game is active, you might lose.")) { clearInterval(clockInterval); switchView('view-home'); } | |
| } | |
| const puz=[{f:'r1b1k2r/ppppnppp/2n2q2/2b5/3NP3/2P1B3/PP3PPP/RN1QKB1R w KQkq - 0 1',s:'d4b5'},{f:'6k1/1p3ppp/p7/8/8/2Q5/q4PPP/2R3K1 w - - 0 1',s:'c3c8'},{f:'r2qk2r/pb1nbppp/1pn1p3/2ppP3/3P4/2PB1NN1/PP3PPP/R1BQK2R w KQkq - 1 10',s:'d3h7'},{f:'4r1k1/1p3ppp/p7/8/8/2Q5/q4PPP/2R3K1 w - - 0 1',s:'c3c8'},{f:'r1bq1rk1/1pp2ppp/p1np1n2/2b1p3/2B1P3/2NP1N2/PPP2PPP/R1BQ1RK1 w - - 0 1',s:'c1g5'}]; | |
| function loadPuzzles(){ | |
| let h=''; puz.forEach((p,i)=>{ | |
| h+=`<div class="menu-item" onclick="playPuz(${i})"> | |
| <div class="menu-icon-box" style="background:rgba(245, 158, 11, 0.2); border-color: rgba(245, 158, 11, 0.4);"><i class="fa-solid fa-puzzle-piece" style="color:var(--warning); filter: drop-shadow(0 0 8px rgba(245, 158, 11, 0.4));"></i></div> | |
| <div class="menu-text"><h3>Puzzle #${i+1}</h3><p>Find the correct move</p></div> | |
| </div>`}); | |
| $('#puzzle-container').html(h); | |
| } | |
| function playPuz(i){ | |
| isBot=false; isAnalysisMode=false; isFastEvaluating=false; game.load(puz[i].f); | |
| setupLiveGameUI(); $('#game-header-text').text('Puzzle'); | |
| $('#game-opp-name').text(`Puzzle #${i+1}`); $('#game-my-name').text("Solve it"); | |
| $('#game-actions-box, #game-my-clock, #game-opp-clock').hide(); | |
| switchView('view-game'); | |
| setTimeout(() => { | |
| board = Chessboard('board',{ | |
| draggable: true, position: puz[i].f, pieceTheme: 'https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png', | |
| onDrop: (s,t) => { | |
| if(s+t === puz[i].s) { toast("Correct!"); setTimeout(()=>switchView('view-puzzles'),1000); } | |
| else { toast("Wrong move!"); return 'snapback'; } | |
| } | |
| }); $(window).resize(); | |
| }, 50); | |
| } | |
| </script> | |
| </body> | |
| </html> |