Commit cb7dd082 authored by Mahmoud Aglan's avatar Mahmoud Aglan

feat: match history — real data from DB with opponent names, result colors, rating changes

New API: /api/match-history.php
- Fetches completed matches where player was white OR black
- Enriches with opponent display names from profiles
- Returns: result, time_control, bot_id, rating changes, move count, date
- Supports game_key filter and limit parameter

History scene rewritten:
- Shows real matches with colored left border (green=win, red=loss, gold=draw)
- Opponent name (or bot name if vs bot)
- Rating change (+12 / -8) in matching color
- Time control label (5+0, 10+0, etc.)
- Relative time (5د, 2س, 3ي)
- Proper result detection for all match_result enum values
- Empty state with helpful message
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent cc7da7a7
<?php
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { http_response_code(200); exit; }
require_once __DIR__ . '/../includes/supabase.php';
require_once __DIR__ . '/../includes/auth.php';
$token = requireAuth();
$userId = getUserId($token);
$limit = min(intval($_GET['limit'] ?? 20), 50);
$gameKey = $_GET['game_key'] ?? null;
$sdb = supabaseService();
// Build filter — get matches where player was white OR black
$filter = "or=(white_player_id.eq.{$userId},black_player_id.eq.{$userId})";
$url = SUPABASE_REST . '/matches?'
. $filter
. '&status=eq.completed'
. '&select=id,game_key,white_player_id,black_player_id,result,time_control,bot_id,rating_change_white,rating_change_black,move_count,created_at'
. '&order=created_at.desc'
. '&limit=' . $limit;
if ($gameKey) {
$url .= '&game_key=eq.' . urlencode($gameKey);
}
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'apikey: ' . SUPABASE_SERVICE_KEY,
'Authorization: Bearer ' . SUPABASE_SERVICE_KEY,
'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$matches = json_decode($response, true);
if ($httpCode >= 400 || !is_array($matches)) {
jsonResponse(['matches' => []]);
}
// Enrich with opponent names
$opponentIds = [];
foreach ($matches as $m) {
$oppId = $m['white_player_id'] === $userId ? $m['black_player_id'] : $m['white_player_id'];
if ($oppId && !in_array($oppId, $opponentIds)) $opponentIds[] = $oppId;
}
$names = [];
if (!empty($opponentIds)) {
$idList = implode(',', $opponentIds);
$profiles = $sdb->get('profiles', [
'id' => "in.({$idList})",
'select' => 'id,username,display_name'
]);
if (is_array($profiles) && !isset($profiles['error'])) {
foreach ($profiles as $p) {
$names[$p['id']] = $p['display_name'] ?? $p['username'] ?? 'Player';
}
}
}
// Add opponent names to matches
foreach ($matches as &$m) {
$isWhite = $m['white_player_id'] === $userId;
$oppId = $isWhite ? $m['black_player_id'] : $m['white_player_id'];
if ($oppId && isset($names[$oppId])) {
if ($isWhite) $m['black_name'] = $names[$oppId];
else $m['white_name'] = $names[$oppId];
}
}
jsonResponse(['matches' => $matches]);
......@@ -22,14 +22,88 @@ export async function mountHistory(el) {
el.querySelector('#back-btn').addEventListener('click', () => { audio.play('click'); scene.pop(); });
try {
const token = store.get('auth.token');
const res = await fetch('/api/leaderboard.php?game_key=chess&limit=1', {
headers: { 'Authorization': 'Bearer ' + token }
});
// For now show a placeholder since we don't have a match history endpoint yet
const list = el.querySelector('#history-list');
list.innerHTML = `<p style="color:#94a3b8;text-align:center;padding:32px;">لا توجد مباريات سابقة</p>`;
const data = await net.get('match-history.php', { limit: 20 });
const matches = data.matches || [];
renderHistory(el, matches);
} catch (e) {
el.querySelector('#history-list').innerHTML = `<p style="color:#ef4444;text-align:center;">${t('common.error')}</p>`;
}
}
function renderHistory(el, matches) {
const list = el.querySelector('#history-list');
const userId = store.get('auth.userId');
if (matches.length === 0) {
list.innerHTML = `
<div style="text-align:center;padding:40px;">
<div style="font-size:40px;margin-bottom:8px;opacity:0.4;">📋</div>
<div style="font-size:14px;color:#64748b;">لا توجد مباريات بعد — العب لتظهر هنا</div>
</div>`;
return;
}
list.innerHTML = matches.map(m => {
const isWhite = m.white_player_id === userId;
const myResult = getMyResult(m.result, isWhite);
const resultConfig = {
win: { label: 'فوز', color: '#34D399', icon: '🏆' },
loss: { label: 'خسارة', color: '#F87171', icon: '💀' },
draw: { label: 'تعادل', color: '#E4AC38', icon: '🤝' },
unknown: { label: '—', color: '#64748b', icon: '•' }
};
const cfg = resultConfig[myResult] || resultConfig.unknown;
const ratingChange = isWhite ? m.rating_change_white : m.rating_change_black;
const ratingStr = ratingChange ? (ratingChange > 0 ? `+${ratingChange}` : `${ratingChange}`) : '';
const timeAgo = formatTimeAgo(m.created_at);
const opponent = isWhite ? (m.black_name || m.bot_id || 'خصم') : (m.white_name || m.bot_id || 'خصم');
const tc = formatTimeControl(m.time_control);
return `
<div style="display:flex;align-items:center;gap:12px;padding:12px;background:#1a1a2e;border-radius:12px;border-right:4px solid ${cfg.color};">
<div style="font-size:20px;">${cfg.icon}</div>
<div style="flex:1;">
<div style="display:flex;justify-content:space-between;align-items:center;">
<span style="font-size:13px;font-weight:600;color:#f8fafc;">${opponent}</span>
<span style="font-size:12px;font-weight:700;color:${cfg.color};font-family:Inter,monospace;">${ratingStr}</span>
</div>
<div style="display:flex;gap:8px;margin-top:3px;">
<span style="font-size:10px;color:#64748b;">${cfg.label}</span>
<span style="font-size:10px;color:#475569;">${tc}</span>
<span style="font-size:10px;color:#475569;">${timeAgo}</span>
</div>
</div>
</div>
`;
}).join('');
}
function getMyResult(result, isWhite) {
if (!result) return 'unknown';
if (result === 'draw' || result === 'mutual_draw' || result === 'stalemate' ||
result === 'threefold_repetition' || result === 'fifty_moves' || result === 'insufficient_material') return 'draw';
if ((result === 'white_wins' && isWhite) || (result === 'black_wins' && !isWhite) ||
(result === 'black_resign' && isWhite) || (result === 'white_resign' && !isWhite) ||
(result === 'black_timeout' && isWhite) || (result === 'white_timeout' && !isWhite) ||
(result === 'black_abandon' && isWhite) || (result === 'white_abandon' && !isWhite)) return 'win';
if (result === 'aborted') return 'unknown';
return 'loss';
}
function formatTimeControl(tc) {
if (!tc) return '';
const labels = { bullet_1_0: '1+0', bullet_2_1: '2+1', blitz_3_0: '3+0', blitz_5_0: '5+0', blitz_5_3: '5+3', rapid_10_0: '10+0', rapid_15_10: '15+10', rapid_30_0: '30+0' };
return labels[tc] || tc.replace(/_/g, ' ');
}
function formatTimeAgo(date) {
if (!date) return '';
const diff = Date.now() - new Date(date).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'الآن';
if (mins < 60) return `${mins}د`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}س`;
const days = Math.floor(hours / 24);
return `${days}ي`;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment