feat: Add recent performance stability stats (matches/days) to player profile
This commit is contained in:
251
web/templates/opponents/detail.html
Normal file
251
web/templates/opponents/detail.html
Normal file
@@ -0,0 +1,251 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
<!-- 1. Header & Summary -->
|
||||
<div class="bg-white dark:bg-slate-800 shadow-xl rounded-2xl overflow-hidden border border-gray-100 dark:border-slate-700 p-8">
|
||||
<div class="flex flex-col md:flex-row items-center md:items-start gap-8">
|
||||
<!-- Avatar -->
|
||||
<div class="flex-shrink-0">
|
||||
{% if player.avatar_url %}
|
||||
<img class="h-32 w-32 rounded-2xl object-cover border-4 border-white shadow-lg" src="{{ player.avatar_url }}">
|
||||
{% else %}
|
||||
<div class="h-32 w-32 rounded-2xl bg-gradient-to-br from-red-100 to-red-200 flex items-center justify-center text-red-600 font-bold text-4xl border-4 border-white shadow-lg">
|
||||
{{ player.username[:2]|upper if player.username else '??' }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="flex-1 text-center md:text-left">
|
||||
<div class="flex items-center justify-center md:justify-start gap-3 mb-2">
|
||||
<h1 class="text-3xl font-black text-gray-900 dark:text-white">{{ player.username }}</h1>
|
||||
<span class="px-2.5 py-0.5 rounded-md text-xs font-bold bg-gray-100 text-gray-600 dark:bg-slate-700 dark:text-gray-300 font-mono">
|
||||
OPPONENT
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-sm font-mono text-gray-500 mb-6">{{ player.steam_id_64 }}</p>
|
||||
|
||||
<!-- Summary Stats -->
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
<div class="bg-gray-50 dark:bg-slate-700/50 p-4 rounded-xl border border-gray-100 dark:border-slate-600">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Matches vs Us</div>
|
||||
<div class="text-2xl font-black text-gray-900 dark:text-white">{{ history|length }}</div>
|
||||
</div>
|
||||
|
||||
{% set wins = history | selectattr('is_win') | list | length %}
|
||||
{% set wr = (wins / history|length * 100) if history else 0 %}
|
||||
<div class="bg-gray-50 dark:bg-slate-700/50 p-4 rounded-xl border border-gray-100 dark:border-slate-600">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Their Win Rate</div>
|
||||
<div class="text-2xl font-black {% if wr > 50 %}text-red-500{% else %}text-green-500{% endif %}">
|
||||
{{ "%.1f"|format(wr) }}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set avg_rating = history | map(attribute='rating') | sum / history|length if history else 0 %}
|
||||
<div class="bg-gray-50 dark:bg-slate-700/50 p-4 rounded-xl border border-gray-100 dark:border-slate-600">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Their Avg Rating</div>
|
||||
<div class="text-2xl font-black text-gray-900 dark:text-white">{{ "%.2f"|format(avg_rating) }}</div>
|
||||
</div>
|
||||
|
||||
{% set avg_kd_diff = history | map(attribute='kd_diff') | sum / history|length if history else 0 %}
|
||||
<div class="bg-gray-50 dark:bg-slate-700/50 p-4 rounded-xl border border-gray-100 dark:border-slate-600">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-1">Avg K/D Diff</div>
|
||||
<div class="text-2xl font-black {% if avg_kd_diff > 0 %}text-red-500{% else %}text-green-500{% endif %}">
|
||||
{{ "%+.2f"|format(avg_kd_diff) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 2. Charts & Side Analysis -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<!-- ELO Performance Chart -->
|
||||
<div class="lg:col-span-2 bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
|
||||
<span>📈</span> Performance vs ELO Segments
|
||||
</h3>
|
||||
<div class="relative h-80 w-full">
|
||||
<canvas id="eloChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Side Stats -->
|
||||
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl p-6 border border-gray-100 dark:border-slate-700">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white mb-6 flex items-center gap-2">
|
||||
<span>🛡️</span> Side Preference (vs Us)
|
||||
</h3>
|
||||
|
||||
{% macro side_row(label, t_val, ct_val, format_str='{:.2f}') %}
|
||||
<div class="mb-6">
|
||||
<div class="flex justify-between text-xs font-bold text-gray-500 uppercase mb-2">
|
||||
<span>{{ label }}</span>
|
||||
</div>
|
||||
<div class="flex items-end justify-between gap-2 mb-2">
|
||||
<span class="text-2xl font-black text-amber-500">{{ (format_str.format(t_val) if t_val is not none else '—') }}</span>
|
||||
<span class="text-xs font-bold text-gray-400">vs</span>
|
||||
<span class="text-2xl font-black text-blue-500">{{ (format_str.format(ct_val) if ct_val is not none else '—') }}</span>
|
||||
</div>
|
||||
<div class="flex h-2 w-full rounded-full overflow-hidden bg-gray-200 dark:bg-slate-600">
|
||||
{% set has_t = t_val is not none %}
|
||||
{% set has_ct = ct_val is not none %}
|
||||
{% set total = (t_val or 0) + (ct_val or 0) %}
|
||||
{% if total > 0 and has_t and has_ct %}
|
||||
{% set t_pct = ((t_val or 0) / total) * 100 %}
|
||||
<div class="h-full bg-amber-500" style="width: {{ t_pct }}%"></div>
|
||||
<div class="h-full bg-blue-500 flex-1"></div>
|
||||
{% else %}
|
||||
<div class="h-full w-1/2 bg-gray-300"></div>
|
||||
<div class="h-full w-1/2 bg-gray-400"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="flex justify-between text-[10px] font-bold text-gray-400 mt-1">
|
||||
<span>T-Side</span>
|
||||
<span>CT-Side</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{{ side_row('Rating', side_stats.get('rating_t'), side_stats.get('rating_ct')) }}
|
||||
{{ side_row('K/D Ratio', side_stats.get('kd_t'), side_stats.get('kd_ct')) }}
|
||||
|
||||
<div class="mt-8 p-4 bg-gray-50 dark:bg-slate-700/30 rounded-xl text-center">
|
||||
<div class="text-xs font-bold text-gray-400 uppercase mb-1">Rounds Sampled</div>
|
||||
<div class="text-xl font-black text-gray-700 dark:text-gray-200">
|
||||
{{ (side_stats.get('rounds_t', 0) or 0) + (side_stats.get('rounds_ct', 0) or 0) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3. Match History Table -->
|
||||
<div class="bg-white dark:bg-slate-800 shadow-lg rounded-2xl overflow-hidden border border-gray-100 dark:border-slate-700">
|
||||
<div class="p-6 border-b border-gray-100 dark:border-slate-700">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white">Match History (Head-to-Head)</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-slate-700">
|
||||
<thead class="bg-gray-50 dark:bg-slate-700/50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-bold text-gray-500 uppercase tracking-wider">Date / Map</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">Their Result</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">Match Elo</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">Their Rating</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">Their K/D</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">K/D Diff (vs Team)</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-bold text-gray-500 uppercase tracking-wider">K / D</th>
|
||||
<th class="px-6 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-slate-700 bg-white dark:bg-slate-800">
|
||||
{% for m in history %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-bold text-gray-900 dark:text-white">{{ m.map_name }}</div>
|
||||
<div class="text-xs text-gray-500 font-mono">
|
||||
<script>document.write(new Date({{ m.start_time }} * 1000).toLocaleDateString())</script>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded text-[10px] font-black uppercase tracking-wide
|
||||
{% if m.is_win %}bg-green-100 text-green-700 border border-green-200
|
||||
{% else %}bg-red-50 text-red-600 border border-red-100{% endif %}">
|
||||
{{ 'WON' if m.is_win else 'LOST' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-mono text-gray-500">
|
||||
{{ "%.0f"|format(m.elo or 0) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
<span class="text-sm font-bold font-mono">{{ "%.2f"|format(m.rating or 0) }}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-mono text-gray-600 dark:text-gray-400">
|
||||
{{ "%.2f"|format(m.kd_ratio or 0) }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||
{% set diff = m.kd_diff %}
|
||||
<span class="text-sm font-bold font-mono {% if diff > 0 %}text-red-500{% else %}text-green-500{% endif %}">
|
||||
{{ "%+.2f"|format(diff) }}
|
||||
</span>
|
||||
<div class="text-[10px] text-gray-400">vs Team Avg {{ "%.2f"|format(m.other_team_kd or 0) }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-center text-sm font-mono text-gray-500">
|
||||
{{ m.kills }} / {{ m.deaths }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<a href="{{ url_for('matches.detail', match_id=m.match_id) }}" class="text-gray-400 hover:text-yrtv-600 transition">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const eloData = {{ elo_stats | tojson }};
|
||||
const labels = eloData.map(d => d.range);
|
||||
const ratings = eloData.map(d => d.avg_rating);
|
||||
const kds = eloData.map(d => d.avg_kd);
|
||||
|
||||
const ctx = document.getElementById('eloChart').getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Avg Rating',
|
||||
data: ratings,
|
||||
backgroundColor: 'rgba(124, 58, 237, 0.6)',
|
||||
borderColor: 'rgba(124, 58, 237, 1)',
|
||||
borderWidth: 1,
|
||||
yAxisID: 'y'
|
||||
},
|
||||
{
|
||||
type: 'line',
|
||||
label: 'Avg K/D',
|
||||
data: kds,
|
||||
borderColor: 'rgba(234, 179, 8, 1)',
|
||||
borderWidth: 2,
|
||||
tension: 0.3,
|
||||
pointBackgroundColor: '#fff',
|
||||
yAxisID: 'y1'
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
interaction: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'left',
|
||||
title: { display: true, text: 'Rating' },
|
||||
grid: { color: 'rgba(156, 163, 175, 0.1)' }
|
||||
},
|
||||
y1: {
|
||||
type: 'linear',
|
||||
display: true,
|
||||
position: 'right',
|
||||
title: { display: true, text: 'K/D Ratio' },
|
||||
grid: { drawOnChartArea: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user