333 lines
20 KiB
HTML
333 lines
20 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block content %}
|
|
<div class="space-y-6">
|
|
<!-- Profile Header -->
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-6">
|
|
<div class="sm:flex sm:items-center sm:justify-between">
|
|
<div class="sm:flex sm:space-x-5">
|
|
<div class="flex-shrink-0 relative group">
|
|
<!-- Avatar -->
|
|
{% if player.avatar_url %}
|
|
<img src="{{ player.avatar_url }}" class="mx-auto h-24 w-24 rounded-full object-cover border-4 border-white shadow-lg">
|
|
{% else %}
|
|
<div class="mx-auto h-24 w-24 rounded-full bg-yrtv-100 flex items-center justify-center text-yrtv-600 font-bold text-3xl border-4 border-white shadow-lg">
|
|
{{ player.username[:2] | upper if player.username else '??' }}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if session.get('is_admin') %}
|
|
<button onclick="document.getElementById('editProfileModal').classList.remove('hidden')" class="absolute inset-0 flex items-center justify-center bg-black bg-opacity-50 rounded-full opacity-0 group-hover:opacity-100 text-white text-xs transition cursor-pointer">
|
|
Edit
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
|
|
<p class="text-xl font-bold text-gray-900 dark:text-white sm:text-2xl">{{ player.username }}</p>
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">{{ player.steam_id_64 }}</p>
|
|
<div class="mt-2 flex justify-center sm:justify-start space-x-2 items-center flex-wrap">
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
|
{{ player.uid }}
|
|
</span>
|
|
<!-- Tags -->
|
|
{% for tag in metadata.tags %}
|
|
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300">
|
|
{{ tag }}
|
|
{% if session.get('is_admin') %}
|
|
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" class="inline ml-1">
|
|
<input type="hidden" name="admin_action" value="remove_tag">
|
|
<input type="hidden" name="tag" value="{{ tag }}">
|
|
<button type="submit" class="text-gray-400 hover:text-red-500 focus:outline-none">×</button>
|
|
</form>
|
|
{% endif %}
|
|
</span>
|
|
{% endfor %}
|
|
|
|
{% if session.get('is_admin') %}
|
|
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" class="inline-flex items-center">
|
|
<input type="hidden" name="admin_action" value="add_tag">
|
|
<input type="text" name="tag" placeholder="New Tag" class="w-20 text-xs border border-gray-300 rounded px-1 py-0.5 focus:outline-none dark:bg-slate-700 dark:border-slate-600 dark:text-white">
|
|
<button type="submit" class="ml-1 text-xs text-yrtv-600 hover:text-yrtv-800">+</button>
|
|
</form>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if metadata.notes %}
|
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400 italic">"{{ metadata.notes }}"</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="mt-5 flex justify-center sm:mt-0 space-x-2">
|
|
{% if session.get('is_admin') %}
|
|
<button onclick="document.getElementById('editProfileModal').classList.remove('hidden')" class="flex justify-center items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 dark:bg-slate-700 dark:text-white dark:border-slate-600 dark:hover:bg-slate-600">
|
|
Edit Profile
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Modal -->
|
|
<div id="editProfileModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
|
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white dark:bg-slate-800">
|
|
<div class="mt-3 text-center">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white">Edit Profile</h3>
|
|
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" class="mt-2 px-7 py-3" enctype="multipart/form-data">
|
|
<input type="hidden" name="admin_action" value="update_profile">
|
|
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Avatar</label>
|
|
<input type="file" name="avatar" accept="image/*" class="mt-1 block w-full text-sm text-gray-500 file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-yrtv-50 file:text-yrtv-700 hover:file:bg-yrtv-100 dark:text-gray-300 dark:file:bg-slate-700 dark:file:text-white">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 text-left">Supported: JPG, PNG. Will replace existing.</p>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 text-left">Notes</label>
|
|
<textarea name="notes" rows="3" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 dark:bg-slate-700 dark:text-white dark:border-slate-600">{{ metadata.notes }}</textarea>
|
|
</div>
|
|
<div class="items-center px-4 py-3">
|
|
<button type="submit" class="px-4 py-2 bg-yrtv-600 text-white text-base font-medium rounded-md w-full shadow-sm hover:bg-yrtv-700 focus:outline-none focus:ring-2 focus:ring-yrtv-500">
|
|
Save
|
|
</button>
|
|
<button type="button" onclick="document.getElementById('editProfileModal').classList.add('hidden')" class="mt-3 px-4 py-2 bg-gray-100 text-gray-700 text-base font-medium rounded-md w-full shadow-sm hover:bg-gray-200 focus:outline-none focus:ring-2 focus:ring-gray-300 dark:bg-slate-700 dark:text-white dark:hover:bg-slate-600">
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layout Reorder: Trend First -->
|
|
|
|
<!-- Trend Chart (Full Width) -->
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-6">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
|
|
<span class="mr-2">📈</span>近期 Rating 走势 (Trend)
|
|
</h3>
|
|
</div>
|
|
<div class="relative h-72">
|
|
<canvas id="trendChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grid: Stats + Radar -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Left: Stats Cards -->
|
|
<div class="lg:col-span-2 grid grid-cols-2 gap-4 h-fit">
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-4 text-center">
|
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Rating</dt>
|
|
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">{{ "%.2f"|format((features.basic_avg_rating if features else 0) or 0) }}</dd>
|
|
</div>
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-4 text-center">
|
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">K/D</dt>
|
|
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">{{ "%.2f"|format((features.basic_avg_kd if features else 0) or 0) }}</dd>
|
|
</div>
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-4 text-center">
|
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">ADR</dt>
|
|
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">{{ "%.1f"|format((features.basic_avg_adr if features else 0) or 0) }}</dd>
|
|
</div>
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-4 text-center">
|
|
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">KAST</dt>
|
|
<dd class="mt-1 text-3xl font-semibold text-gray-900 dark:text-white">{{ "%.1f"|format((features.basic_avg_kast if features else 0) * 100) }}%</dd>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right: Radar -->
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-6 flex flex-col items-center justify-center">
|
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white mb-4 w-full text-left">能力六维图</h3>
|
|
<div class="relative h-64 w-full">
|
|
<canvas id="radarChart"></canvas>
|
|
</div>
|
|
</div>
|
|
<!-- Match History (L2) -->
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-6">
|
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white mb-4">比赛记录 (History - {{ history|length }})</h3>
|
|
<div class="overflow-x-auto max-h-96 overflow-y-auto">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700 relative">
|
|
<thead class="bg-gray-50 dark:bg-slate-700 sticky top-0">
|
|
<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">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">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">Link</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="bg-white dark:bg-slate-800 divide-y divide-gray-200 dark:divide-gray-700">
|
|
{% for m in history | reverse %}
|
|
<tr>
|
|
<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>
|
|
</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 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 font-medium">
|
|
<a href="{{ url_for('matches.detail', match_id=m.match_id) }}" class="text-yrtv-600 hover:text-yrtv-900">View</a>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="6" class="px-6 py-4 text-center text-gray-500">No recent matches found.</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-slate-800 shadow rounded-lg p-6">
|
|
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white mb-6">玩家评价 ({{ comments|length }})</h3>
|
|
|
|
<!-- Comment Form -->
|
|
<form action="{{ url_for('players.detail', steam_id=player.steam_id_64) }}" method="POST" class="mb-8">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Your Name (Optional)</label>
|
|
<input type="text" name="username" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 dark:bg-slate-700 dark:text-white" placeholder="Anonymous">
|
|
</div>
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">Comment</label>
|
|
<textarea name="content" rows="3" required class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 dark:bg-slate-700 dark:text-white" placeholder="Share your thoughts..."></textarea>
|
|
</div>
|
|
<button type="submit" class="px-4 py-2 bg-yrtv-600 text-white rounded hover:bg-yrtv-700">Submit Review</button>
|
|
</form>
|
|
|
|
<!-- Comment List -->
|
|
<div class="space-y-6">
|
|
{% for comment in comments %}
|
|
<div class="flex space-x-4 p-4 bg-gray-50 dark:bg-slate-700 rounded-lg">
|
|
<div class="flex-shrink-0">
|
|
<span class="inline-block h-10 w-10 rounded-full overflow-hidden bg-gray-100">
|
|
<svg class="h-full w-full text-gray-300" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M24 20.993V24H0v-2.996A14.977 14.977 0 0112.004 15c4.904 0 9.26 2.354 11.996 5.993zM16.002 8.999a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
</span>
|
|
</div>
|
|
<div class="flex-1 space-y-1">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-medium text-gray-900 dark:text-white">{{ comment.username }}</h3>
|
|
<p class="text-sm text-gray-500">{{ comment.created_at }}</p>
|
|
</div>
|
|
<p class="text-sm text-gray-500 dark:text-gray-300">{{ comment.content }}</p>
|
|
|
|
<div class="mt-2 flex items-center space-x-2">
|
|
<button onclick="likeComment({{ comment.id }}, this)" class="flex items-center text-gray-400 hover:text-red-500">
|
|
<svg class="h-5 w-5 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
|
</svg>
|
|
<span class="like-count">{{ comment.likes }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<p class="text-gray-500 text-center">No comments yet. Be the first!</p>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function likeComment(commentId, btn) {
|
|
fetch(`/players/comment/${commentId}/like`, { method: 'POST' })
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
const countSpan = btn.querySelector('.like-count');
|
|
countSpan.innerText = parseInt(countSpan.innerText) + 1;
|
|
btn.classList.add('text-red-500');
|
|
}
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const steamId = "{{ player.steam_id_64 }}";
|
|
|
|
fetch(`/players/${steamId}/charts_data`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Radar Chart
|
|
const ctxRadar = document.getElementById('radarChart').getContext('2d');
|
|
new Chart(ctxRadar, {
|
|
type: 'radar',
|
|
data: {
|
|
labels: ['STA', 'BAT', 'HPS', 'PTL', 'SIDE', 'UTIL'],
|
|
datasets: [{
|
|
label: 'Ability',
|
|
data: [
|
|
data.radar.STA, data.radar.BAT, data.radar.HPS,
|
|
data.radar.PTL, data.radar.SIDE, data.radar.UTIL
|
|
],
|
|
backgroundColor: 'rgba(124, 58, 237, 0.2)',
|
|
borderColor: 'rgba(124, 58, 237, 1)',
|
|
pointBackgroundColor: 'rgba(124, 58, 237, 1)',
|
|
}]
|
|
},
|
|
options: {
|
|
scales: {
|
|
r: {
|
|
beginAtZero: true,
|
|
suggestedMax: 2.0 // Adjust based on data range
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Trend Chart
|
|
const ctxTrend = document.getElementById('trendChart').getContext('2d');
|
|
new Chart(ctxTrend, {
|
|
type: 'line',
|
|
data: {
|
|
labels: data.trend.labels,
|
|
datasets: [{
|
|
label: 'Rating',
|
|
data: data.trend.values,
|
|
borderColor: 'rgba(16, 185, 129, 1)',
|
|
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
tension: 0.1,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: {
|
|
beginAtZero: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|