1.0.2-hotfix: Added matchlist new features.
This commit is contained in:
Binary file not shown.
@@ -23,12 +23,67 @@ def detail(match_id):
|
|||||||
return "Match not found", 404
|
return "Match not found", 404
|
||||||
|
|
||||||
players = StatsService.get_match_players(match_id)
|
players = StatsService.get_match_players(match_id)
|
||||||
|
# Convert sqlite3.Row objects to dicts to allow modification
|
||||||
|
players = [dict(p) for p in players]
|
||||||
|
|
||||||
rounds = StatsService.get_match_rounds(match_id)
|
rounds = StatsService.get_match_rounds(match_id)
|
||||||
|
|
||||||
# Organize players by team
|
# --- Roster Identification ---
|
||||||
|
# Fetch active roster to identify "Our Team" players
|
||||||
|
from web.services.web_service import WebService
|
||||||
|
lineups = WebService.get_lineups()
|
||||||
|
# Assume we use the first/active lineup
|
||||||
|
active_roster_ids = []
|
||||||
|
if lineups:
|
||||||
|
try:
|
||||||
|
active_roster_ids = json.loads(lineups[0]['player_ids_json'])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Mark roster players (Ensure strict string comparison)
|
||||||
|
roster_set = set(str(uid) for uid in active_roster_ids)
|
||||||
|
for p in players:
|
||||||
|
p['is_in_roster'] = str(p['steam_id_64']) in roster_set
|
||||||
|
|
||||||
|
# --- Party Size Calculation ---
|
||||||
|
# Only calculate party size for OUR ROSTER members.
|
||||||
|
# Group roster members by match_team_id
|
||||||
|
roster_parties = {} # match_team_id -> count of roster members
|
||||||
|
|
||||||
|
for p in players:
|
||||||
|
if p['is_in_roster']:
|
||||||
|
mtid = p.get('match_team_id')
|
||||||
|
if mtid and mtid > 0:
|
||||||
|
key = f"tid_{mtid}"
|
||||||
|
roster_parties[key] = roster_parties.get(key, 0) + 1
|
||||||
|
|
||||||
|
# Assign party size ONLY to roster members
|
||||||
|
for p in players:
|
||||||
|
if p['is_in_roster']:
|
||||||
|
mtid = p.get('match_team_id')
|
||||||
|
if mtid and mtid > 0:
|
||||||
|
p['party_size'] = roster_parties.get(f"tid_{mtid}", 1)
|
||||||
|
else:
|
||||||
|
p['party_size'] = 1 # Solo roster player
|
||||||
|
else:
|
||||||
|
p['party_size'] = 0 # Hide party info for non-roster players
|
||||||
|
|
||||||
|
# Organize players by Side (team_id)
|
||||||
|
# team_id 1 = Team 1, team_id 2 = Team 2
|
||||||
|
# Note: group_id 1/2 usually corresponds to Team 1/2.
|
||||||
|
# Fallback to team_id if group_id is missing or 0 (legacy data compatibility)
|
||||||
|
team1_players = [p for p in players if p.get('group_id') == 1]
|
||||||
|
team2_players = [p for p in players if p.get('group_id') == 2]
|
||||||
|
|
||||||
|
# If group_id didn't work (empty lists), try team_id grouping (if team_id is 1/2 only)
|
||||||
|
if not team1_players and not team2_players:
|
||||||
team1_players = [p for p in players if p['team_id'] == 1]
|
team1_players = [p for p in players if p['team_id'] == 1]
|
||||||
team2_players = [p for p in players if p['team_id'] == 2]
|
team2_players = [p for p in players if p['team_id'] == 2]
|
||||||
|
|
||||||
|
# Explicitly sort by Rating DESC
|
||||||
|
team1_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True)
|
||||||
|
team2_players.sort(key=lambda x: x.get('rating', 0) or 0, reverse=True)
|
||||||
|
|
||||||
return render_template('matches/detail.html', match=match,
|
return render_template('matches/detail.html', match=match,
|
||||||
team1_players=team1_players, team2_players=team2_players,
|
team1_players=team1_players, team2_players=team2_players,
|
||||||
rounds=rounds)
|
rounds=rounds)
|
||||||
|
|||||||
@@ -44,6 +44,109 @@ class StatsService:
|
|||||||
|
|
||||||
matches = query_db('l2', sql, args)
|
matches = query_db('l2', sql, args)
|
||||||
|
|
||||||
|
# Enrich matches with Avg ELO, Party info, and Our Team Result
|
||||||
|
if matches:
|
||||||
|
match_ids = [m['match_id'] for m in matches]
|
||||||
|
placeholders = ','.join('?' for _ in match_ids)
|
||||||
|
|
||||||
|
# Fetch ELO
|
||||||
|
elo_sql = f"""
|
||||||
|
SELECT match_id, AVG(group_origin_elo) as avg_elo
|
||||||
|
FROM fact_match_teams
|
||||||
|
WHERE match_id IN ({placeholders}) AND group_origin_elo > 0
|
||||||
|
GROUP BY match_id
|
||||||
|
"""
|
||||||
|
elo_rows = query_db('l2', elo_sql, match_ids)
|
||||||
|
elo_map = {row['match_id']: row['avg_elo'] for row in elo_rows}
|
||||||
|
|
||||||
|
# Fetch Max Party Size
|
||||||
|
party_sql = f"""
|
||||||
|
SELECT match_id, MAX(cnt) as max_party
|
||||||
|
FROM (
|
||||||
|
SELECT match_id, match_team_id, COUNT(*) as cnt
|
||||||
|
FROM fact_match_players
|
||||||
|
WHERE match_id IN ({placeholders}) AND match_team_id > 0
|
||||||
|
GROUP BY match_id, match_team_id
|
||||||
|
)
|
||||||
|
GROUP BY match_id
|
||||||
|
"""
|
||||||
|
party_rows = query_db('l2', party_sql, match_ids)
|
||||||
|
party_map = {row['match_id']: row['max_party'] for row in party_rows}
|
||||||
|
|
||||||
|
# --- New: Determine "Our Team" Result ---
|
||||||
|
# Logic: Check if any player from `active_roster` played in these matches.
|
||||||
|
# Use WebService to get the active roster
|
||||||
|
from web.services.web_service import WebService
|
||||||
|
import json
|
||||||
|
|
||||||
|
lineups = WebService.get_lineups()
|
||||||
|
active_roster_ids = []
|
||||||
|
if lineups:
|
||||||
|
try:
|
||||||
|
# Load IDs and ensure they are all strings for DB comparison consistency
|
||||||
|
raw_ids = json.loads(lineups[0]['player_ids_json'])
|
||||||
|
active_roster_ids = [str(uid) for uid in raw_ids]
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# If no roster, we can't determine "Our Result"
|
||||||
|
if not active_roster_ids:
|
||||||
|
result_map = {}
|
||||||
|
else:
|
||||||
|
roster_placeholders = ','.join('?' for _ in active_roster_ids)
|
||||||
|
|
||||||
|
# We cast steam_id_64 to TEXT to ensure match even if stored as int
|
||||||
|
our_result_sql = f"""
|
||||||
|
SELECT mp.match_id, mp.team_id, m.winner_team, COUNT(*) as our_count
|
||||||
|
FROM fact_match_players mp
|
||||||
|
JOIN fact_matches m ON mp.match_id = m.match_id
|
||||||
|
WHERE mp.match_id IN ({placeholders})
|
||||||
|
AND CAST(mp.steam_id_64 AS TEXT) IN ({roster_placeholders})
|
||||||
|
GROUP BY mp.match_id, mp.team_id
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Combine args: match_ids + roster_ids
|
||||||
|
combined_args = match_ids + active_roster_ids
|
||||||
|
our_rows = query_db('l2', our_result_sql, combined_args)
|
||||||
|
|
||||||
|
# Map match_id -> result ('win', 'loss', 'draw', 'mixed')
|
||||||
|
result_map = {}
|
||||||
|
|
||||||
|
match_sides = {}
|
||||||
|
match_winners = {}
|
||||||
|
|
||||||
|
for r in our_rows:
|
||||||
|
mid = r['match_id']
|
||||||
|
if mid not in match_sides: match_sides[mid] = {}
|
||||||
|
match_sides[mid][r['team_id']] = r['our_count']
|
||||||
|
match_winners[mid] = r['winner_team']
|
||||||
|
|
||||||
|
for mid, sides in match_sides.items():
|
||||||
|
winner = match_winners.get(mid)
|
||||||
|
if not winner:
|
||||||
|
result_map[mid] = 'draw'
|
||||||
|
continue
|
||||||
|
|
||||||
|
our_on_winner = sides.get(winner, 0)
|
||||||
|
loser = 2 if winner == 1 else 1
|
||||||
|
our_on_loser = sides.get(loser, 0)
|
||||||
|
|
||||||
|
if our_on_winner > 0 and our_on_loser == 0:
|
||||||
|
result_map[mid] = 'win'
|
||||||
|
elif our_on_loser > 0 and our_on_winner == 0:
|
||||||
|
result_map[mid] = 'loss'
|
||||||
|
elif our_on_winner > 0 and our_on_loser > 0:
|
||||||
|
result_map[mid] = 'mixed'
|
||||||
|
else:
|
||||||
|
result_map[mid] = None
|
||||||
|
|
||||||
|
# Convert to dict to modify
|
||||||
|
matches = [dict(m) for m in matches]
|
||||||
|
for m in matches:
|
||||||
|
m['avg_elo'] = elo_map.get(m['match_id'], 0)
|
||||||
|
m['max_party'] = party_map.get(m['match_id'], 1)
|
||||||
|
m['our_result'] = result_map.get(m['match_id'])
|
||||||
|
|
||||||
# Count total for pagination
|
# Count total for pagination
|
||||||
count_sql = f"SELECT COUNT(*) as cnt FROM fact_matches WHERE {where_str}"
|
count_sql = f"SELECT COUNT(*) as cnt FROM fact_matches WHERE {where_str}"
|
||||||
total = query_db('l2', count_sql, args[:-2], one=True)['cnt']
|
total = query_db('l2', count_sql, args[:-2], one=True)['cnt']
|
||||||
@@ -243,20 +346,25 @@ class StatsService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_player_trend(steam_id, limit=20):
|
def get_player_trend(steam_id, limit=20):
|
||||||
sql = """
|
# We need party_size: count of players with same match_team_id in the same match
|
||||||
SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id, m.map_name
|
# Using a correlated subquery for party_size
|
||||||
FROM fact_match_players mp
|
|
||||||
JOIN fact_matches m ON mp.match_id = m.match_id
|
|
||||||
WHERE mp.steam_id_64 = ?
|
|
||||||
ORDER BY m.start_time ASC
|
|
||||||
"""
|
|
||||||
# We fetch all then slice last 'limit' in python or use subquery.
|
|
||||||
# DESC LIMIT gets recent, but we want chronological for chart.
|
|
||||||
# So: SELECT ... ORDER BY time DESC LIMIT ? -> then reverse in code.
|
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT * FROM (
|
SELECT * FROM (
|
||||||
SELECT m.start_time, mp.rating, mp.kd_ratio, mp.adr, m.match_id, m.map_name, mp.is_win
|
SELECT
|
||||||
|
m.start_time,
|
||||||
|
mp.rating,
|
||||||
|
mp.kd_ratio,
|
||||||
|
mp.adr,
|
||||||
|
m.match_id,
|
||||||
|
m.map_name,
|
||||||
|
mp.is_win,
|
||||||
|
mp.match_team_id,
|
||||||
|
(SELECT COUNT(*)
|
||||||
|
FROM fact_match_players p2
|
||||||
|
WHERE p2.match_id = mp.match_id
|
||||||
|
AND p2.match_team_id = mp.match_team_id
|
||||||
|
AND p2.match_team_id > 0 -- Ensure we don't count 0 (solo default) as a massive party
|
||||||
|
) as party_size
|
||||||
FROM fact_match_players mp
|
FROM fact_match_players mp
|
||||||
JOIN fact_matches m ON mp.match_id = m.match_id
|
JOIN fact_matches m ON mp.match_id = m.match_id
|
||||||
WHERE mp.steam_id_64 = ?
|
WHERE mp.steam_id_64 = ?
|
||||||
|
|||||||
@@ -47,12 +47,35 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0 h-8 w-8">
|
<div class="flex-shrink-0 h-8 w-8">
|
||||||
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url or 'https://via.placeholder.com/32' }}" alt="">
|
{% if p.avatar_url %}
|
||||||
|
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt="">
|
||||||
|
{% else %}
|
||||||
|
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200">
|
||||||
|
{{ (p.username or p.steam_id_64)[:2] | upper }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
|
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
|
||||||
{{ p.username or p.steam_id_64 }}
|
{{ p.username or p.steam_id_64 }}
|
||||||
</a>
|
</a>
|
||||||
|
{% if p.party_size > 1 %}
|
||||||
|
{% set pc = p.party_size %}
|
||||||
|
{% set p_color = 'bg-blue-100 text-blue-800' %}
|
||||||
|
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
|
||||||
|
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
|
||||||
|
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
|
||||||
|
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
|
||||||
|
{% endif %}
|
||||||
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
|
||||||
|
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
|
||||||
|
</svg>
|
||||||
|
{{ p.party_size }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@@ -97,12 +120,35 @@
|
|||||||
<td class="px-6 py-4 whitespace-nowrap">
|
<td class="px-6 py-4 whitespace-nowrap">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="flex-shrink-0 h-8 w-8">
|
<div class="flex-shrink-0 h-8 w-8">
|
||||||
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url or 'https://via.placeholder.com/32' }}" alt="">
|
{% if p.avatar_url %}
|
||||||
|
<img class="h-8 w-8 rounded-full" src="{{ p.avatar_url }}" alt="">
|
||||||
|
{% else %}
|
||||||
|
<div class="h-8 w-8 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-xs border border-yrtv-200">
|
||||||
|
{{ (p.username or p.steam_id_64)[:2] | upper }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
|
<a href="{{ url_for('players.detail', steam_id=p.steam_id_64) }}" class="text-sm font-medium text-gray-900 dark:text-white hover:text-yrtv-600">
|
||||||
{{ p.username or p.steam_id_64 }}
|
{{ p.username or p.steam_id_64 }}
|
||||||
</a>
|
</a>
|
||||||
|
{% if p.party_size > 1 %}
|
||||||
|
{% set pc = p.party_size %}
|
||||||
|
{% set p_color = 'bg-blue-100 text-blue-800' %}
|
||||||
|
{% if pc == 2 %}{% set p_color = 'bg-indigo-100 text-indigo-800' %}
|
||||||
|
{% elif pc == 3 %}{% set p_color = 'bg-blue-100 text-blue-800' %}
|
||||||
|
{% elif pc == 4 %}{% set p_color = 'bg-purple-100 text-purple-800' %}
|
||||||
|
{% elif pc >= 5 %}{% set p_color = 'bg-orange-100 text-orange-800' %}
|
||||||
|
{% endif %}
|
||||||
|
<span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium {{ p_color }} dark:bg-opacity-20" title="Roster Party of {{ p.party_size }}">
|
||||||
|
<svg class="mr-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path d="M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z" />
|
||||||
|
</svg>
|
||||||
|
{{ p.party_size }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -17,6 +17,8 @@
|
|||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时间</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时间</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">地图</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">地图</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">比分</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">比分</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ELO</th>
|
||||||
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Party</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时长</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">时长</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">操作</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -31,13 +33,62 @@
|
|||||||
{{ match.map_name }}
|
{{ match.map_name }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
|
||||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 1 %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
|
<div class="flex items-center space-x-2">
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 1 %}bg-green-100 text-green-800 border border-green-200{% else %}bg-gray-100 text-gray-500{% endif %}">
|
||||||
{{ match.score_team1 }}
|
{{ match.score_team1 }}
|
||||||
|
{% if match.winner_team == 1 %}
|
||||||
|
<svg class="ml-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20"><path d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" /></svg>
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
-
|
<span class="text-gray-400">-</span>
|
||||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 2 %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if match.winner_team == 2 %}bg-green-100 text-green-800 border border-green-200{% else %}bg-gray-100 text-gray-500{% endif %}">
|
||||||
{{ match.score_team2 }}
|
{{ match.score_team2 }}
|
||||||
|
{% if match.winner_team == 2 %}
|
||||||
|
<svg class="ml-1 h-3 w-3" fill="currentColor" viewBox="0 0 20 20"><path d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z" /></svg>
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<!-- Our Team Result Badge -->
|
||||||
|
{% if match.our_result %}
|
||||||
|
{% if match.our_result == 'win' %}
|
||||||
|
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-green-500 text-white">
|
||||||
|
VICTORY
|
||||||
|
</span>
|
||||||
|
{% elif match.our_result == 'loss' %}
|
||||||
|
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-red-500 text-white">
|
||||||
|
DEFEAT
|
||||||
|
</span>
|
||||||
|
{% elif match.our_result == 'mixed' %}
|
||||||
|
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-yellow-500 text-white">
|
||||||
|
CIVIL WAR
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{% if match.avg_elo and match.avg_elo > 0 %}
|
||||||
|
<span class="font-mono">{{ "%.0f"|format(match.avg_elo) }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-xs text-gray-300">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{% if match.max_party and match.max_party > 1 %}
|
||||||
|
{% set p = match.max_party %}
|
||||||
|
{% set party_class = 'bg-gray-100 text-gray-800' %}
|
||||||
|
{% if p == 2 %} {% set party_class = 'bg-indigo-100 text-indigo-800 border border-indigo-200' %}
|
||||||
|
{% elif p == 3 %} {% set party_class = 'bg-blue-100 text-blue-800 border border-blue-200' %}
|
||||||
|
{% elif p == 4 %} {% set party_class = 'bg-purple-100 text-purple-800 border border-purple-200' %}
|
||||||
|
{% elif p >= 5 %} {% set party_class = 'bg-orange-100 text-orange-800 border border-orange-200' %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ party_class }}">
|
||||||
|
👥 {{ match.max_party }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-xs text-gray-300">Solo</span>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||||
{{ (match.duration / 60) | int }} min
|
{{ (match.duration / 60) | int }} min
|
||||||
|
|||||||
@@ -26,10 +26,14 @@
|
|||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||||
{% for player in players %}
|
{% for player in players %}
|
||||||
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-4 flex flex-col items-center hover:shadow-lg transition">
|
<div class="bg-gray-50 dark:bg-slate-700 rounded-lg p-4 flex flex-col items-center hover:shadow-lg transition">
|
||||||
<!-- Avatar Hidden/Placeholder -->
|
<!-- Avatar -->
|
||||||
<div class="h-20 w-20 rounded-full mb-4 bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-2xl">
|
{% if player.avatar_url %}
|
||||||
|
<img class="h-20 w-20 rounded-full mb-4 object-cover border-4 border-white shadow-sm" src="{{ player.avatar_url }}" alt="{{ player.username }}">
|
||||||
|
{% else %}
|
||||||
|
<div class="h-20 w-20 rounded-full mb-4 bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-2xl border-4 border-white shadow-sm">
|
||||||
{{ player.username[:2] | upper if player.username else '??' }}
|
{{ player.username[:2] | upper if player.username else '??' }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ player.username }}</h3>
|
<h3 class="text-lg font-medium text-gray-900 dark:text-white">{{ player.username }}</h3>
|
||||||
<p class="text-sm text-gray-500 mb-2">{{ player.steam_id_64 }}</p>
|
<p class="text-sm text-gray-500 mb-2">{{ player.steam_id_64 }}</p>
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,8 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Date</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Date</th>
|
||||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Map</th>
|
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Map</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Result</th>
|
||||||
|
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Party</th>
|
||||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rating</th>
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Rating</th>
|
||||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">K/D</th>
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">K/D</th>
|
||||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ADR</th>
|
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">ADR</th>
|
||||||
@@ -157,12 +159,33 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
|
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||||
{% for m in history %}
|
{% for m in history | reverse %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
|
||||||
<script>document.write(new Date({{ m.start_time }} * 1000).toLocaleDateString())</script>
|
<script>document.write(new Date({{ m.start_time }} * 1000).toLocaleDateString())</script>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ m.map_name }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">{{ m.map_name }}</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-center">
|
||||||
|
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {% if m.is_win %}bg-green-100 text-green-800{% else %}bg-red-100 text-red-800{% endif %}">
|
||||||
|
{{ 'WIN' if m.is_win else 'LOSS' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-6 py-4 whitespace-nowrap text-center text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{% if m.party_size and m.party_size > 1 %}
|
||||||
|
{% set p = m.party_size %}
|
||||||
|
{% set party_class = 'bg-gray-100 text-gray-800' %}
|
||||||
|
{% if p == 2 %} {% set party_class = 'bg-indigo-100 text-indigo-800' %}
|
||||||
|
{% elif p == 3 %} {% set party_class = 'bg-blue-100 text-blue-800' %}
|
||||||
|
{% elif p == 4 %} {% set party_class = 'bg-purple-100 text-purple-800' %}
|
||||||
|
{% elif p >= 5 %} {% set party_class = 'bg-orange-100 text-orange-800' %}
|
||||||
|
{% endif %}
|
||||||
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium {{ party_class }}">
|
||||||
|
👥 {{ m.party_size }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-xs text-gray-400">Solo</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-right font-bold {% if (m.rating or 0) >= 1.1 %}text-green-600{% elif (m.rating or 0) < 0.9 %}text-red-600{% else %}text-gray-900 dark:text-white{% endif %}">{{ "%.2f"|format(m.rating or 0) }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-right font-bold {% if (m.rating or 0) >= 1.1 %}text-green-600{% elif (m.rating or 0) < 0.9 %}text-red-600{% else %}text-gray-900 dark:text-white{% endif %}">{{ "%.2f"|format(m.rating or 0) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.2f"|format(m.kd_ratio or 0) }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.2f"|format(m.kd_ratio or 0) }}</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(m.adr or 0) }}</td>
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-gray-500 dark:text-gray-400">{{ "%.1f"|format(m.adr or 0) }}</td>
|
||||||
|
|||||||
Reference in New Issue
Block a user