Spaces:
Running
Running
| <html lang="tr"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Akıllı Futbol Analiz & Tahmin Platformu</title> | |
| <!-- Bootstrap 5 CSS --> | |
| <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <!-- Font Awesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- Chart.js --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| /* Custom CSS for modern look */ | |
| :root { | |
| --primary-color: #1e3a8a; | |
| --secondary-color: #3b82f6; | |
| --success-color: #10b981; | |
| --warning-color: #f59e0b; | |
| --danger-color: #ef4444; | |
| } | |
| body { | |
| background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| } | |
| .header-gradient { | |
| background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%); | |
| color: white; | |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); | |
| } | |
| .live-badge { | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { | |
| 0% { opacity: 1; } | |
| 50% { opacity: 0.7; } | |
| 100% { opacity: 1; } | |
| } | |
| .card { | |
| border: none; | |
| border-radius: 15px; | |
| box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); | |
| transition: transform 0.2s; | |
| } | |
| .card:hover { | |
| transform: translateY(-2px); | |
| } | |
| .prediction-bar { | |
| height: 30px; | |
| border-radius: 15px; | |
| margin-bottom: 10px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .prediction-fill { | |
| height: 100%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: white; | |
| font-weight: bold; | |
| transition: width 1s ease-in-out; | |
| } | |
| .team-form-badge { | |
| width: 30px; | |
| height: 30px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 50%; | |
| margin: 0 2px; | |
| font-weight: bold; | |
| font-size: 0.8rem; | |
| } | |
| .form-win { background-color: var(--success-color); color: white; } | |
| .form-draw { background-color: var(--warning-color); color: white; } | |
| .form-loss { background-color: var(--danger-color); color: white; } | |
| .match-row:hover { | |
| background-color: #f8fafc; | |
| cursor: pointer; | |
| } | |
| .loading-spinner { | |
| display: none; | |
| } | |
| .stats-card { | |
| background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <nav class="navbar navbar-expand-lg header-gradient py-3"> | |
| <div class="container"> | |
| <a class="navbar-brand text-white fw-bold fs-4" href="#"> | |
| <i class="fas fa-futbol me-2"></i>Akıllı Futbol Analiz & Tahmin Platformu | |
| </a> | |
| <div class="d-flex align-items-center gap-3"> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="text-white text-decoration-none small bg-dark bg-opacity-25 px-3 py-1 rounded"> | |
| <i class="fas fa-code me-1"></i>Built with anycoder | |
| </a> | |
| <span class="badge bg-success live-badge px-3 py-2"> | |
| <i class="fas fa-circle me-1" style="font-size: 8px;"></i>7/24 Canlı Veri | |
| </span> | |
| </div> | |
| </div> | |
| </nav> | |
| <div class="container my-4"> | |
| <!-- Control Panel --> | |
| <div class="row mb-4"> | |
| <div class="col-12"> | |
| <div class="card"> | |
| <div class="card-header bg-white border-bottom-0 pt-4"> | |
| <h5 class="card-title mb-0"> | |
| <i class="fas fa-filter text-primary me-2"></i>Filtreleme ve Kontrol | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="row align-items-end"> | |
| <div class="col-md-4 mb-3"> | |
| <label class="form-label">Lig Seçimi</label> | |
| <select class="form-select" id="leagueFilter"> | |
| <option value="all">Tüm Ligler</option> | |
| <!-- Options will be populated dynamically --> | |
| </select> | |
| </div> | |
| <div class="col-md-4 mb-3"> | |
| <label class="form-label">Manuel Takım Analizi</label> | |
| <div class="input-group"> | |
| <input type="text" class="form-control" id="manualHomeTeam" placeholder="Ev Sahibi Takım"> | |
| <input type="text" class="form-control" id="manualAwayTeam" placeholder="Deplasman Takım"> | |
| </div> | |
| </div> | |
| <div class="col-md-4 mb-3"> | |
| <button class="btn btn-primary w-100" onclick="analyzeManualTeams()"> | |
| <i class="fas fa-search me-2"></i>Manuel Analiz Yap | |
| </button> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-12"> | |
| <button class="btn btn-outline-primary" onclick="refreshData()"> | |
| <i class="fas fa-sync-alt me-2"></i>Verileri Yenile | |
| </button> | |
| <span class="text-muted ms-3 small" id="lastUpdateTime"> | |
| <i class="fas fa-clock me-1"></i>Son Güncelleme: - | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <!-- Upcoming Matches --> | |
| <div class="col-lg-5 mb-4"> | |
| <div class="card h-100"> | |
| <div class="card-header bg-white border-bottom-0 pt-4 d-flex justify-content-between align-items-center"> | |
| <h5 class="card-title mb-0"> | |
| <i class="fas fa-calendar-alt text-primary me-2"></i>Önümüzdeki Maçlar | |
| </h5> | |
| <div class="loading-spinner text-primary" id="matchesLoading"> | |
| <i class="fas fa-spinner fa-spin"></i> | |
| </div> | |
| </div> | |
| <div class="card-body p-0"> | |
| <div class="table-responsive"> | |
| <table class="table table-hover mb-0" id="matchesTable"> | |
| <thead class="table-light"> | |
| <tr> | |
| <th>Tarih & Saat</th> | |
| <th>Lig</th> | |
| <th>Ev Sahibi</th> | |
| <th>Deplasman</th> | |
| <th>İşlem</th> | |
| </tr> | |
| </thead> | |
| <tbody id="matchesTableBody"> | |
| <!-- Matches will be populated here --> | |
| </tbody> | |
| </table> | |
| </div> | |
| <div id="noMatchesMessage" class="text-center py-5 text-muted" style="display: none;"> | |
| <i class="fas fa-inbox fa-3x mb-3 opacity-25"></i> | |
| <p>Maç bulunamadı</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Analysis Dashboard --> | |
| <div class="col-lg-7 mb-4"> | |
| <div class="card h-100" id="analysisCard" style="display: none;"> | |
| <div class="card-header bg-white border-bottom-0 pt-4"> | |
| <h5 class="card-title mb-0"> | |
| <i class="fas fa-chart-line text-primary me-2"></i>Analiz Panosu | |
| </h5> | |
| </div> | |
| <div class="card-body"> | |
| <!-- Match Info --> | |
| <div class="text-center mb-4 p-3 bg-light rounded"> | |
| <h3 class="mb-3"> | |
| <span id="analysisHomeTeam" class="fw-bold text-primary">-</span> | |
| <span class="mx-3 text-muted">vs</span> | |
| <span id="analysisAwayTeam" class="fw-bold text-danger">-</span> | |
| </h3> | |
| <p class="text-muted mb-0" id="analysisMatchInfo">-</p> | |
| </div> | |
| <!-- Predictions --> | |
| <div class="mb-4"> | |
| <h6 class="fw-bold mb-3"><i class="fas fa-percentage me-2"></i>Tahmin Olasılıkları</h6> | |
| <div class="mb-2"> | |
| <div class="d-flex justify-content-between mb-1"> | |
| <span id="homeWinLabel">Ev Sahibi Kazanır</span> | |
| <span class="fw-bold" id="homeWinPercent">0%</span> | |
| </div> | |
| <div class="prediction-bar bg-light"> | |
| <div class="prediction-fill bg-primary" id="homeWinBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="mb-2"> | |
| <div class="d-flex justify-content-between mb-1"> | |
| <span>Beraberlik</span> | |
| <span class="fw-bold" id="drawPercent">0%</span> | |
| </div> | |
| <div class="prediction-bar bg-light"> | |
| <div class="prediction-fill bg-warning" id="drawBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="mb-2"> | |
| <div class="d-flex justify-content-between mb-1"> | |
| <span id="awayWinLabel">Deplasman Kazanır</span> | |
| <span class="fw-bold" id="awayWinPercent">0%</span> | |
| </div> | |
| <div class="prediction-bar bg-light"> | |
| <div class="prediction-fill bg-danger" id="awayWinBar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Charts --> | |
| <div class="row mb-4"> | |
| <div class="col-md-6 mb-3"> | |
| <canvas id="homeTeamChart"></canvas> | |
| </div> | |
| <div class="col-md-6 mb-3"> | |
| <canvas id="awayTeamChart"></canvas> | |
| </div> | |
| </div> | |
| <!-- Detailed Stats --> | |
| <div class="row"> | |
| <div class="col-md-6 mb-3"> | |
| <div class="card stats-card h-100"> | |
| <div class="card-body"> | |
| <h6 class="card-title text-primary" id="homeStatsTitle">Ev Sahibi İstatistikleri</h6> | |
| <ul class="list-unstyled mb-0 mt-3"> | |
| <li class="mb-2"><i class="fas fa-futbol me-2 text-muted"></i>Ort. Gol: <span id="homeAvgGoals" class="fw-bold">-</span></li> | |
| <li class="mb-2"><i class="fas fa-shield-alt me-2 text-muted"></i>Ort. Yenilen Gol: <span id="homeAvgConceded" class="fw-bold">-</span></li> | |
| <li class="mb-2"><i class="fas fa-chart-bar me-2 text-muted"></i>Form: <span id="homeForm" class="fw-bold">-</span></li> | |
| <li><i class="fas fa-trophy me-2 text-muted"></i>Puan Ort.: <span id="homeAvgPoints" class="fw-bold">-</span></li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-6 mb-3"> | |
| <div class="card stats-card h-100"> | |
| <div class="card-body"> | |
| <h6 class="card-title text-danger" id="awayStatsTitle">Deplasman İstatistikleri</h6> | |
| <ul class="list-unstyled mb-0 mt-3"> | |
| <li class="mb-2"><i class="fas fa-futbol me-2 text-muted"></i>Ort. Gol: <span id="awayAvgGoals" class="fw-bold">-</span></li> | |
| <li class="mb-2"><i class="fas fa-shield-alt me-2 text-muted"></i>Ort. Yenilen Gol: <span id="awayAvgConceded" class="fw-bold">-</span></li> | |
| <li class="mb-2"><i class="fas fa-chart-bar me-2 text-muted"></i>Form: <span id="awayForm" class="fw-bold">-</span></li> | |
| <li><i class="fas fa-trophy me-2 text-muted"></i>Puan Ort.: <span id="awayAvgPoints" class="fw-bold">-</span></li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Head to Head --> | |
| <div class="mt-3" id="h2hSection" style="display: none;"> | |
| <h6 class="fw-bold mb-3"><i class="fas fa-history me-2"></i>Önceki Karşılaşmalar</h6> | |
| <div class="table-responsive"> | |
| <table class="table table-sm table-bordered" id="h2hTable"> | |
| <thead class="table-light"> | |
| <tr> | |
| <th>Tarih</th> | |
| <th>Ev Sahibi</th> | |
| <th>Skor</th> | |
| <th>Deplasman</th> | |
| </tr> | |
| </thead> | |
| <tbody id="h2hTableBody"></tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary w-100 mt-3" onclick="refreshCurrentAnalysis()"> | |
| <i class="fas fa-sync-alt me-2"></i>Analizi Yenile | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Empty State --> | |
| <div class="card h-100" id="emptyAnalysisCard"> | |
| <div class="card-body d-flex flex-column justify-content-center align-items-center text-center p-5"> | |
| <i class="fas fa-chart-pie fa-4x text-muted mb-3 opacity-25"></i> | |
| <h5 class="text-muted">Analiz için bir maç seçin</h5> | |
| <p class="text-muted small">Sol taraftan bir maç seçerek veya yukarıdan manuel takım girerek analiz başlatabilirsiniz.</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Live Data Indicator --> | |
| <div class="row"> | |
| <div class="col-12"> | |
| <div class="alert alert-info d-flex align-items-center mb-4" role="alert"> | |
| <i class="fas fa-info-circle me-2"></i> | |
| <div> | |
| <strong>Canlı Veri:</strong> Veriler her 60 saniyede bir otomatik olarak güncellenmektedir. | |
| <span class="ms-2 badge bg-primary" id="nextUpdateCountdown">Sonraki güncelleme: 60s</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Disclaimer --> | |
| <div class="row"> | |
| <div class="col-12"> | |
| <div class="alert alert-warning" role="alert"> | |
| <i class="fas fa-exclamation-triangle me-2"></i> | |
| <strong>Uyarı:</strong> Bu platform Football-Data.org'dan alınan gerçek verilerle istatistiksel simülasyon yapar. | |
| Kesin bir tahmin aracı veya bahis önerisi <strong>DEĞİLDİR</strong>. Sonuçlar garanti etmez. | |
| Sorumlu oyun oynayınız. | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Configuration | |
| const API_TOKEN = '326546a82e2848f1b66d2e859a8f0832'; | |
| const API_BASE_URL = 'https://api.football-data.org/v4'; | |
| let currentMatches = []; | |
| let selectedMatch = null; | |
| let homeTeamChart = null; | |
| let awayTeamChart = null; | |
| let autoRefreshInterval = null; | |
| let countdownInterval = null; | |
| let countdownValue = 60; | |
| // DOM Elements | |
| const matchesTableBody = document.getElementById('matchesTableBody'); | |
| const leagueFilter = document.getElementById('leagueFilter'); | |
| const lastUpdateTime = document.getElementById('lastUpdateTime'); | |
| const matchesLoading = document.getElementById('matchesLoading'); | |
| const noMatchesMessage = document.getElementById('noMatchesMessage'); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initializeApp(); | |
| }); | |
| async function initializeApp() { | |
| try { | |
| await fetchUpcomingMatches(); | |
| startAutoRefresh(); | |
| updateLastUpdateTime(); | |
| } catch (error) { | |
| console.error('Initialization error:', error); | |
| showError('Uygulama başlatılırken bir hata oluştu. Lütfen sayfayı yenileyin.'); | |
| } | |
| } | |
| // Fetch upcoming matches for next 7 days | |
| async function fetchUpcomingMatches() { | |
| showLoading(true); | |
| try { | |
| // Calculate date range (today to +7 days) | |
| const today = new Date(); | |
| const nextWeek = new Date(today); | |
| nextWeek.setDate(today.getDate() + 7); | |
| const dateFrom = formatDateForAPI(today); | |
| const dateTo = formatDateForAPI(nextWeek); | |
| // Fetch matches from API | |
| const response = await fetch(`${API_BASE_URL}/matches?dateFrom=${dateFrom}&dateTo=${dateTo}`, { | |
| headers: { | |
| 'X-Auth-Token': API_TOKEN | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`API Error: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| currentMatches = data.matches || []; | |
| // Populate league filter | |
| populateLeagueFilter(currentMatches); | |
| // Display matches | |
| displayMatches(currentMatches); | |
| updateLastUpdateTime(); | |
| } catch (error) { | |
| console.error('Error fetching matches:', error); | |
| showError('Maç verileri çekilirken bir hata oluştu. API limiti aşılmış olabilir veya bağlantı hatası vardır.'); | |
| } finally { | |
| showLoading(false); | |
| } | |
| } | |
| // Format date for API (YYYY-MM-DD) | |
| function formatDateForAPI(date) { | |
| return date.toISOString().split('T')[0]; | |
| } | |
| // Format date for display | |
| function formatDateForDisplay(dateString) { | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString('tr-TR', { | |
| day: '2-digit', | |
| month '2-digit', | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }); | |
| } | |
| // Populate league filter dropdown | |
| function populateLeagueFilter(matches) { | |
| const competitions = [...new Set(matches.map(m => m.competition.name))].sort(); | |
| const currentValue = leagueFilter.value; | |
| leagueFilter.innerHTML = '<option value="all">Tüm Ligler</option>'; | |
| competitions.forEach(comp => { | |
| const option = document.createElement('option'); | |
| option.value = comp; | |
| option.textContent = comp; | |
| leagueFilter.appendChild(option); | |
| }); | |
| leagueFilter.value = currentValue; | |
| } | |
| // Display matches in table | |
| function displayMatches(matches) { | |
| matchesTableBody.innerHTML = ''; | |
| if (matches.length === 0) { | |
| noMatchesMessage.style.display = 'block'; | |
| return; | |
| } | |
| noMatchesMessage.style.display = 'none'; | |
| matches.forEach(match => { | |
| const row = document.createElement('tr'); | |
| row.className = 'match-row'; | |
| const homeTeam = match.homeTeam.name; | |
| const awayTeam = match.awayTeam.name; | |
| const competition = match.competition.name; | |
| const matchDate = formatDateForDisplay(match.utcDate); | |
| row.innerHTML = ` | |
| <td class="align-middle">${matchDate}</td> | |
| <td class="align-middle"><span class="badge bg-secondary">${competition}</span></td> | |
| <td class="align-middle fw-bold">${homeTeam}</td> | |
| <td class="align-middle fw-bold">${awayTeam}</td> | |
| <td class="align-middle"> | |
| <button class="btn btn-sm btn-primary" onclick="analyzeMatch('${match.id}')"> | |
| <i class="fas fa-chart-bar me-1"></i>Analiz Et | |
| </button> | |
| </td> | |
| `; | |
| matchesTableBody.appendChild(row); | |
| }); | |
| } | |
| // Filter matches by league | |
| leagueFilter.addEventListener('change', (e) => { | |
| const selectedLeague = e.target.value; | |
| if (selectedLeague === 'all') { | |
| displayMatches(currentMatches); | |
| } else { | |
| const filtered = currentMatches.filter(m => m.competition.name === selectedLeague); | |
| displayMatches(filtered); | |
| } | |
| }); | |
| // Analyze specific match | |
| async function analyzeMatch(matchId) { | |
| const match = currentMatches.find(m => m.id == matchId); | |
| if (!match) return; | |
| selectedMatch = match; | |
| document.getElementById('analysisHomeTeam').textContent = match.homeTeam.name; | |
| document.getElementById('analysisAwayTeam').textContent = match.awayTeam.name; | |
| document.getElementById('analysisMatchInfo').textContent = `${match.competition.name} - ${formatDateForDisplay(match.utcDate)}`; | |
| await performAnalysis(match.homeTeam.id, match.awayTeam.id, match.homeTeam.name, match.awayTeam.name); | |
| } | |
| // Manual team analysis | |
| async function analyzeManualTeams() { | |
| const homeTeamName = document.getElementById('manualHomeTeam').value.trim(); | |
| const awayTeamName = document.getElementById('manualAwayTeam').value.trim(); | |
| if (!homeTeamName || !awayTeamName) { | |
| alert('Lütfen her iki takımın da adını girin.'); | |
| return; | |
| } | |
| document.getElementById('analysisHomeTeam').textContent = homeTeamName; | |
| document.getElementById('analysisAwayTeam').textContent = awayTeamName; | |
| document.getElementById('analysisMatchInfo').textContent = 'Manuel Analiz'; | |
| // For manual analysis, we'll try to find team IDs from current matches or use a generic approach | |
| // Since we don't have a search endpoint in the free tier easily accessible without team IDs, | |
| // we'll simulate the analysis with placeholder data or try to match from existing matches | |
| // Try to find matching teams in current matches | |
| const homeMatch = currentMatches.find(m => | |
| m.homeTeam.name.toLowerCase().includes(homeTeamName.toLowerCase()) || | |
| m.awayTeam.name.toLowerCase().includes(homeTeamName.toLowerCase()) | |
| ); | |
| const awayMatch = currentMatches.find(m => | |
| m.homeTeam.name.toLowerCase().includes(awayTeamName.toLowerCase()) || | |
| m.awayTeam.name.toLowerCase().includes(awayTeamName.toLowerCase()) | |
| ); | |
| if (homeMatch && awayMatch) { | |
| const homeId = homeMatch.homeTeam.name.toLowerCase().includes(homeTeamName.toLowerCase()) ? | |
| homeMatch.homeTeam.id : homeMatch.awayTeam.id; | |
| const awayId = awayMatch.homeTeam.name.toLowerCase().includes(awayTeamName.toLowerCase()) ? | |
| awayMatch.homeTeam.id : awayMatch.awayTeam.id; | |
| await performAnalysis(homeId, awayId, homeTeamName, awayTeamName); | |
| } else { | |
| // If teams not found in current matches, show alert | |
| alert('Takımlar mevcut maç listesinde bulunamadı. Lütfen listeden bir maç seçin veya geçerli takım adları girin.'); | |
| } | |
| } | |
| // Perform analysis for two teams | |
| async function performAnalysis(homeTeamId, awayTeamId, homeTeamName, awayTeamName) { | |
| showAnalysisLoading(true); | |
| try { | |
| // Fetch last 5 matches for both teams | |
| const [homeMatches, awayMatches] = await Promise.all([ | |
| fetchTeamMatches(homeTeamId), | |
| fetchTeamMatches(awayTeamId) | |
| ]); | |
| // Calculate statistics | |
| const homeStats = calculateTeamStats(homeMatches, homeTeamId); | |
| const awayStats = calculateTeamStats(awayMatches, awayTeamId); | |
| // Calculate predictions | |
| const predictions = calculatePredictions(homeStats, awayStats); | |
| // Update UI | |
| updateAnalysisUI(homeStats, awayStats, predictions, homeTeamName, awayTeamName); | |
| // Fetch and display H2H if possible | |
| await fetchHeadToHead(homeTeamId, awayTeamId); | |
| // Show analysis card | |
| document.getElementById('analysisCard').style.display = 'block'; | |
| document.getElementById('emptyAnalysisCard').style.display = 'none'; | |
| } catch (error) { | |
| console.error('Analysis error:', error); | |
| showError('Analiz yapılırken bir hata oluştu.'); | |
| } finally { | |
| showAnalysisLoading(false); | |
| } | |
| } | |
| // Fetch team matches (finished matches) | |
| async function fetchTeamMatches(teamId) { | |
| const today = new Date(); | |
| const lastMonth = new Date(today); | |
| lastMonth.setMonth(today.getMonth() - 3); // Look back 3 months to ensure we get 5 matches | |
| const dateFrom = formatDateForAPI(lastMonth); | |
| const dateTo = formatDateForAPI(today); | |
| const response = await fetch(`${API_BASE_URL}/teams/${teamId}/matches?dateFrom=${dateFrom}&dateTo=${dateTo}&status=FINISHED`, { | |
| headers: { | |
| 'X-Auth-Token': API_TOKEN | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`API Error: ${response.status}`); | |
| } | |
| const data = await response.json(); | |
| return data.matches ? data.matches.slice(0, 5) : []; // Last 5 matches | |
| } | |
| // Calculate team statistics from matches | |
| function calculateTeamStats(matches, teamId) { | |
| if (!matches || matches.length === 0) { | |
| return { | |
| matches: [], | |
| avgGoals: 0, | |
| avgConceded: 0, | |
| avgPoints: 0, | |
| form: '', | |
| goalsScored: [], | |
| goalsConceded: [], | |
| results: [] | |
| }; | |
| } | |
| let totalGoals = 0; | |
| let totalConceded = 0; | |
| let totalPoints = 0; | |
| let form = ''; | |
| const goalsScored = []; | |
| const goalsConceded = []; | |
| const results = []; | |
| matches.forEach(match => { | |
| const isHome = match.homeTeam.id == teamId; | |
| const teamScore = isHome ? match.score.fullTime.home : match.score.fullTime.away; | |
| const opponentScore = isHome ? match.score.fullTime.away : match.score.fullTime.home; | |
| totalGoals += teamScore; | |
| totalConceded += opponentScore; | |
| goalsScored.push(teamScore); | |
| goalsConceded.push(opponentScore); | |
| let result; | |
| if (teamScore > opponentScore) { | |
| totalPoints += 3; | |
| result = 'W'; | |
| form += 'G-'; | |
| } else if (teamScore === opponentScore) { | |
| totalPoints += 1; | |
| result = 'D'; | |
| form += 'B-'; | |
| } else { | |
| result = 'L'; | |
| form += 'M-'; | |
| } | |
| results.push(result); | |
| }); | |
| const matchCount = matches.length; | |
| form = form.slice(0, -1); // Remove last dash | |
| return { | |
| matches: matches, | |
| avgGoals: (totalGoals / matchCount).toFixed(2), | |
| avgConceded: (totalConceded / matchCount).toFixed(2), | |
| avgPoints: (totalPoints / matchCount).toFixed(2), | |
| form: form, | |
| goalsScored: goalsScored.reverse(), // Most recent last | |
| goalsConceded: goalsConceded.reverse(), | |
| results: results.reverse() | |
| }; | |
| } | |
| // Calculate prediction probabilities | |
| function calculatePredictions(homeStats, awayStats) { | |
| // Simple weighted algorithm | |
| // Factors: Points average (40%), Goals scored/conceded difference (30%), Home advantage (20%), Recent form (10%) | |
| // Normalize stats (handle empty data) | |
| const homePoints = parseFloat(homeStats.avgPoints) || 1.5; | |
| const awayPoints = parseFloat(awayStats.avgPoints) || 1.5; | |
| const homeGoals = parseFloat(homeStats.avgGoals) || 1; | |
| const awayGoals = parseFloat(awayStats.avgGoals) || 1; | |
| const homeConceded = parseFloat(homeStats.avgConceded) || 1; | |
| const awayConceded = parseFloat(awayStats.avgConceded) || 1; | |
| // Points factor (40%) | |
| const totalPoints = homePoints + awayPoints; | |
| const homePointsProb = (homePoints / totalPoints) * 40; | |
| const awayPointsProb = (awayPoints / totalPoints) * 40; | |
| // Goals factor (30%) - based on attack vs defense | |
| const homeAttackStrength = homeGoals / (homeGoals + awayConceded); | |
| const awayAttackStrength = awayGoals / (awayGoals + homeConceded); | |
| const totalAttack = homeAttackStrength + awayAttackStrength; | |
| const homeGoalsProb = (homeAttackStrength / totalAttack) * 30; | |
| const awayGoalsProb = (awayAttackStrength / totalAttack) * 30; | |
| // Home advantage (20%) | |
| const homeAdvantage = 12; // 60% of 20% | |
| const awayDisadvantage = 8; // 40% of 20% | |
| // Form factor (10%) - based on last 5 results | |
| const homeFormScore = calculateFormScore(homeStats.results); | |
| const awayFormScore = calculateFormScore(awayStats.results); | |
| const totalForm = homeFormScore + awayFormScore || 2; | |
| const homeFormProb = (homeFormScore / totalForm) * 10; | |
| const awayFormProb = (awayFormScore / totalForm) * 10; | |
| // Calculate raw probabilities | |
| let homeWin = homePointsProb + homeGoalsProb + homeAdvantage + homeFormProb; | |
| let awayWin = awayPointsProb + awayGoalsProb + awayDisadvantage + awayFormProb; | |
| // Normalize to ensure they sum to less than 100, leaving room for draw | |
| const totalWin = homeWin + awayWin; | |
| const normalizationFactor = 85 / totalWin; // Reserve 15% for draw minimum | |
| homeWin = homeWin * normalizationFactor; | |
| awayWin = awayWin * normalizationFactor; | |
| // Draw probability based on how close the teams are | |
| const diff = Math.abs(homeWin - awayWin); | |
| let draw = 15 + (diff < 10 ? 10 : 0) - (diff > 20 ? 5 : 0); | |
| // Adjust win probabilities to ensure sum is 100 | |
| const remaining = 100 - draw; | |
| const winTotal = homeWin + awayWin; | |
| homeWin = (homeWin / winTotal) * remaining; | |
| awayWin = (awayWin / winTotal) * remaining; | |
| return { | |
| homeWin: Math.round(homeWin), | |
| draw: Math.round(draw), | |
| awayWin: Math.round(awayWin) | |
| }; | |
| } | |
| // Calculate form score from results array (W, D, L) | |
| function calculateFormScore(results) { | |
| if (!results || results.length === 0) return 1; | |
| let score = 0; | |
| // Weight recent matches more | |
| const weights = [1, 1.1, 1.2, 1.3, 1.4]; | |
| results.forEach((result, index) => { | |
| const weight = weights[index] || 1; | |
| if (result === 'W') score += 3 * weight; | |
| else if (result === 'D') score += 1 * weight; | |
| else score += 0; | |
| }); | |
| return score / results.length; | |
| } | |
| // Update analysis UI | |
| function updateAnalysisUI(homeStats, awayStats, predictions, homeName, awayName) { | |
| // Update prediction bars | |
| document.getElementById('homeWinLabel').textContent = `${homeName} Kazanır`; | |
| document.getElementById('awayWinLabel').textContent = `${awayName} Kazanır`; | |
| document.getElementById('homeWinPercent').textContent = `${predictions.homeWin}%`; | |
| document.getElementById('drawPercent').textContent = `${predictions.draw}%`; | |
| document.getElementById('awayWinPercent').textContent = `${predictions.awayWin}%`; | |
| // Animate bars | |
| setTimeout(() => { | |
| document.getElementById('homeWinBar').style.width = `${predictions.homeWin}%`; | |
| document.getElementById('drawBar').style.width = `${predictions.draw}%`; | |
| document.getElementById('awayWinBar').style.width = `${predictions.awayWin}%`; | |
| }, 100); | |
| // Update stats | |
| document.getElementById('homeStatsTitle').textContent = `${homeName} İstatistikleri`; | |
| document.getElementById('awayStatsTitle').textContent = `${awayName} İstatistikleri`; | |
| document.getElementById('homeAvgGoals').textContent = homeStats.avgGoals; | |
| document.getElementById('homeAvgConceded').textContent = homeStats.avgConceded; | |
| document.getElementById('homeAvgPoints').textContent = homeStats.avgPoints; | |
| document.getElementById('awayAvgGoals').textContent = awayStats.avgGoals; | |
| document.getElementById('awayAvgConceded').textContent = awayStats.avgConceded; | |
| document.getElementById('awayAvgPoints').textContent = awayStats.avgPoints; | |
| // Update form badges | |
| document.getElementById('homeForm').innerHTML = createFormBadges(homeStats.form); | |
| document.getElementById('awayForm').innerHTML = createFormBadges(awayStats.form); | |
| // Update charts | |
| updateCharts(homeStats, awayStats, homeName, awayName); | |
| } | |
| // Create form badges HTML | |
| function createFormBadges(formString) { | |
| if (!formString) return '-'; | |
| const parts = formString.split('-'); | |
| let html = ''; | |
| parts.forEach(part => { | |
| let className = ''; | |
| let text = ''; | |
| switch(part) { | |
| case 'G': | |
| className = 'form-win'; | |
| text = 'G'; | |
| break; | |
| case 'B': | |
| className = 'form-draw'; | |
| text = 'B'; | |
| break; | |
| case 'M': | |
| className = 'form-loss'; | |
| text = 'M'; | |
| break; | |
| } | |
| html += `<span class="team-form-badge ${className}">${text}</span>`; | |
| }); | |
| return html; | |
| } | |
| // Update charts | |
| function updateCharts(homeStats, awayStats, homeName, awayName) { | |
| // Destroy existing charts if they exist | |
| if (homeTeamChart) homeTeamChart.destroy(); | |
| if (awayTeamChart) awayTeamChart.destroy(); | |
| // Home team chart | |
| const homeCtx = document.getElementById('homeTeamChart').getContext('2d'); | |
| homeTeamChart = new Chart(homeCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: ['Maç 1', 'Maç 2', 'Maç 3', 'Maç 4', 'Maç 5 (Son)'], | |
| datasets: [{ | |
| label: 'Atılan Gol', | |
| data: homeStats.goalsScored, | |
| backgroundColor: 'rgba(59, 130, 246, 0.7)', | |
| borderColor: 'rgba(59, 130, 246, 1)', | |
| borderWidth: 1 | |
| }, { | |
| label: 'Yenilen Gol', | |
| data: homeStats.goalsConceded, | |
| backgroundColor: 'rgba(239, 68, 68, 0.7)', | |
| borderColor: 'rgba(239, 68, 68, 1)', | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: `${homeName} - Son 5 Maç` | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| ticks: { | |
| stepSize: 1 | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Away team chart | |
| const awayCtx = document.getElementById('awayTeamChart').getContext('2d'); | |
| awayTeamChart = new Chart(awayCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: ['Maç 1', 'Maç 2', 'Maç 3', 'Maç 4', 'Maç 5 (Son)'], | |
| datasets: [{ | |
| label: 'Atılan Gol', | |
| data: awayStats.goalsScored, | |
| backgroundColor: 'rgba(59, 130, 246, 0.7)', | |
| borderColor: 'rgba(59, 130, 246, 1)', | |
| borderWidth: 1 | |
| }, { | |
| label: 'Yenilen Gol', | |
| data: awayStats.goalsConceded, | |
| backgroundColor: 'rgba(239, 68, 68, 0.7)', | |
| borderColor: 'rgba(239, 68, 68, 1)', | |
| borderWidth: 1 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| title: { | |
| display: true, | |
| text: `${awayName} - Son 5 Maç` | |
| } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| ticks: { | |
| stepSize: 1 | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Fetch head to head matches | |
| async function fetchHeadToHead(team1Id, team2Id) { | |
| try { | |
| // Note: H2H endpoint might require different permissions in free tier | |
| // We'll try to find H2H from the matches we already have or fetch recent matches between them | |
| const today = new Date(); | |
| const lastYear = new Date(today); | |
| lastYear.setFullYear(today.getFullYear() - 1); | |
| const response = await fetch(`${API_BASE_URL}/matches?dateFrom=${formatDateForAPI(lastYear)}&dateTo=${formatDateForAPI(today)}&status=FINISHED`, { | |
| headers: { | |
| 'X-Auth-Token': API_TOKEN | |
| } | |
| }); | |
| if (!response.ok) return; | |
| const data = await response.json(); | |
| const allMatches = data.matches || []; | |
| // Filter matches between these two teams | |
| const h2hMatches = allMatches.filter(m => | |
| (m.homeTeam.id == team1Id && m.awayTeam.id == team2Id) || | |
| (m.homeTeam.id == team2Id && m.awayTeam.id == team1Id) | |
| ).slice(0, 5); // Last 5 H2H | |
| if (h2hMatches.length > 0) { | |
| displayH2H(h2hMatches); | |
| } else { | |
| document.getElementById('h2hSection').style.display = 'none'; | |
| } | |
| } catch (error) { | |
| console.error('H2H fetch error:', error); | |
| document.getElementById('h2hSection').style.display = 'none'; | |
| } | |
| } | |
| // Display H2H matches | |
| function displayH2H(matches) { | |
| const tbody = document.getElementById('h2hTableBody'); | |
| tbody.innerHTML = ''; | |
| matches.forEach(match => { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td>${formatDateForDisplay(match.utcDate)}</td> | |
| <td>${match.homeTeam.name}</td> | |
| <td class="fw-bold">${match.score.fullTime.home} - ${match.score.fullTime.away}</td> | |
| <td>${match.awayTeam.name}</td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| document.getElementById('h2hSection').style.display = 'block'; | |
| } | |
| // Refresh current analysis | |
| async function refreshCurrentAnalysis() { | |
| if (selectedMatch) { | |
| await analyzeMatch(selectedMatch.id); | |
| } | |
| } | |
| // Refresh all data | |
| async function refreshData() { | |
| await fetchUpcomingMatches(); | |
| if (selectedMatch) { | |
| await refreshCurrentAnalysis(); | |
| } | |
| } | |
| // Auto refresh every 60 seconds | |
| function startAutoRefresh() { | |
| // Clear existing intervals if any | |
| if (autoRefreshInterval) clearInterval(autoRefreshInterval); | |
| if (countdownInterval) clearInterval(countdownInterval); | |
| countdownValue = 60; | |
| updateCountdown(); | |
| countdownInterval = setInterval(() => { | |
| countdownValue--; | |
| updateCountdown(); | |
| if (countdownValue <= 0) countdownValue = 60; | |
| }, 1000); | |
| autoRefreshInterval = setInterval(() => { | |
| refreshData(); | |
| countdownValue = 60; | |
| }, 60000); | |
| } | |
| function updateCountdown() { | |
| document.getElementById('nextUpdateCountdown').textContent = `Sonraki güncelleme: ${countdownValue}s`; | |
| } | |
| // Update last update time display | |
| function updateLastUpdateTime() { | |
| const now = new Date(); | |
| lastUpdateTime.innerHTML = `<i class="fas fa-clock me-1"></i>Son Güncelleme: ${now.toLocaleTimeString('tr-TR')}`; | |
| } | |
| // Show/hide loading states | |
| function showLoading(show) { | |
| matchesLoading.style.display = show ? 'block' : 'none'; | |
| } | |
| function showAnalysisLoading(show) { |