279 lines
16 KiB
HTML
279 lines
16 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}My Team - Clubhouse{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8" x-data="clubhouse()">
|
|
<!-- Header -->
|
|
<div class="md:flex md:items-center md:justify-between mb-8">
|
|
<div class="flex-1 min-w-0">
|
|
<h2 class="text-2xl font-bold leading-7 text-gray-900 dark:text-white sm:text-3xl sm:truncate">
|
|
<span x-text="team.name || 'My Team'"></span>
|
|
<span class="ml-2 text-sm font-normal text-gray-500" x-text="team.description"></span>
|
|
</h2>
|
|
</div>
|
|
<div class="mt-4 flex md:mt-0 md:ml-4">
|
|
{% if session.get('is_admin') %}
|
|
<button @click="showScoutModal = true" type="button" class="ml-3 inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-yrtv-600 hover:bg-yrtv-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yrtv-500">
|
|
<span class="mr-2">🔍</span> Scout Player
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sorting Controls -->
|
|
<div class="flex justify-end mb-4">
|
|
<div class="inline-flex shadow-sm rounded-md" role="group">
|
|
<button type="button" @click="sortBy('rating')" :class="{'bg-yrtv-600 text-white': currentSort === 'rating', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'rating'}" class="px-4 py-2 text-sm font-medium border border-gray-200 rounded-l-lg dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
|
|
Rating
|
|
</button>
|
|
<button type="button" @click="sortBy('kd')" :class="{'bg-yrtv-600 text-white': currentSort === 'kd', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'kd'}" class="px-4 py-2 text-sm font-medium border-t border-b border-gray-200 dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
|
|
K/D
|
|
</button>
|
|
<button type="button" @click="sortBy('matches')" :class="{'bg-yrtv-600 text-white': currentSort === 'matches', 'bg-white text-gray-700 hover:bg-gray-50': currentSort !== 'matches'}" class="px-4 py-2 text-sm font-medium border border-gray-200 rounded-r-lg dark:bg-slate-700 dark:border-slate-600 dark:text-white dark:hover:bg-slate-600">
|
|
Matches
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Roster (Grid) -->
|
|
<div class="mb-10">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-4">Active Roster</h3>
|
|
<!-- Dynamic Grid based on roster size, default to 5 slots + 1 add button -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-6">
|
|
<!-- Render Actual Roster -->
|
|
<template x-for="(player, index) in roster" :key="player.steam_id_64">
|
|
<div class="relative bg-white dark:bg-slate-800 rounded-lg shadow-md border border-gray-200 dark:border-slate-600 h-80 flex flex-col items-center justify-center p-4 transition hover:border-yrtv-400">
|
|
|
|
<div class="w-full h-full flex flex-col items-center">
|
|
<div class="relative w-32 h-32 mb-4">
|
|
<!-- Avatar Logic: Image or Initials -->
|
|
<template x-if="player.avatar_url">
|
|
<img :src="player.avatar_url" class="w-32 h-32 rounded-full object-cover border-4 border-yrtv-500 shadow-lg">
|
|
</template>
|
|
<template x-if="!player.avatar_url">
|
|
<div class="w-32 h-32 rounded-full bg-yrtv-100 flex items-center justify-center border-4 border-yrtv-500 shadow-lg text-yrtv-600 font-bold text-4xl">
|
|
<span x-text="(player.username || player.name || player.steam_id_64).substring(0, 2).toUpperCase()"></span>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<h4 class="text-lg font-bold text-gray-900 dark:text-white truncate w-full text-center" x-text="player.username || player.name || player.steam_id_64"></h4>
|
|
<div class="flex flex-wrap justify-center gap-1 mb-4 min-h-[1.5rem]">
|
|
<template x-for="tag in (player.tags || [])">
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300" x-text="tag"></span>
|
|
</template>
|
|
<template x-if="!player.tags || player.tags.length === 0">
|
|
<span class="text-xs text-gray-400 italic">No tags</span>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- Stats Grid -->
|
|
<div class="grid grid-cols-2 gap-2 w-full text-center mb-auto">
|
|
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
|
<div class="text-xs text-gray-400">Rating</div>
|
|
<div class="font-bold text-yrtv-600 dark:text-yrtv-400" x-text="(player.stats?.basic_avg_rating || 0).toFixed(2)"></div>
|
|
</div>
|
|
<div class="bg-gray-50 dark:bg-slate-700 rounded p-1">
|
|
<div class="text-xs text-gray-400">K/D</div>
|
|
<div class="font-bold" x-text="(player.stats?.basic_avg_kd || 0).toFixed(2)"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="flex space-x-2 mt-2">
|
|
<a :href="'/players/' + player.steam_id_64" class="text-yrtv-600 hover:text-yrtv-800 text-sm font-medium">Profile</a>
|
|
{% if session.get('is_admin') %}
|
|
<button @click="removePlayer(player.steam_id_64)" class="text-red-500 hover:text-red-700 text-sm font-medium">Release</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Add Player Slot (Only for Admin) -->
|
|
{% if session.get('is_admin') %}
|
|
<div class="relative bg-gray-50 dark:bg-slate-800/50 rounded-lg shadow-sm border-2 border-dashed border-gray-300 dark:border-slate-600 h-80 flex flex-col items-center justify-center p-4 hover:border-yrtv-400 transition cursor-pointer" @click="showScoutModal = true">
|
|
<div class="w-16 h-16 rounded-full bg-white dark:bg-slate-700 flex items-center justify-center mb-3 group-hover:bg-yrtv-100 dark:group-hover:bg-slate-600 transition">
|
|
<svg class="w-8 h-8 text-gray-400 group-hover:text-yrtv-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
|
|
</div>
|
|
<span class="text-sm font-medium text-gray-500 dark:text-gray-400 group-hover:text-yrtv-600">Add Player</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bench / Extended Roster (Hidden as logic is merged into main grid) -->
|
|
<!-- The grid above now handles unlimited players, so we remove the separate Bench section to avoid duplication -->
|
|
|
|
<!-- Scout Modal -->
|
|
<div x-show="showScoutModal" class="fixed inset-0 z-10 overflow-y-auto" style="display: none;">
|
|
<div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
|
|
<div class="fixed inset-0 transition-opacity" aria-hidden="true" @click="showScoutModal = false">
|
|
<div class="absolute inset-0 bg-gray-500 opacity-75"></div>
|
|
</div>
|
|
|
|
<span class="hidden sm:inline-block sm:align-middle sm:h-screen" aria-hidden="true">​</span>
|
|
|
|
<div class="inline-block align-bottom bg-white dark:bg-slate-800 rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-lg w-full">
|
|
<div class="bg-white dark:bg-slate-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 dark:text-white mb-4">Scout New Player</h3>
|
|
|
|
<!-- Search Input -->
|
|
<div class="mt-2 relative rounded-md shadow-sm">
|
|
<input type="text" x-model="searchQuery" @input.debounce.300ms="searchPlayers()" placeholder="Search by name..." class="focus:ring-yrtv-500 focus:border-yrtv-500 block w-full pl-4 pr-12 sm:text-sm border-gray-300 dark:bg-slate-700 dark:border-slate-600 dark:text-white rounded-md h-12">
|
|
</div>
|
|
|
|
<!-- Results List -->
|
|
<div class="mt-4 max-h-60 overflow-y-auto">
|
|
<template x-if="searchResults.length === 0 && searchQuery.length > 1">
|
|
<p class="text-sm text-gray-500 text-center py-4">No players found.</p>
|
|
</template>
|
|
|
|
<ul class="divide-y divide-gray-200 dark:divide-slate-700">
|
|
<template x-for="player in searchResults" :key="player.steam_id">
|
|
<li class="py-3 flex items-center justify-between hover:bg-gray-50 dark:hover:bg-slate-700 px-2 rounded cursor-pointer">
|
|
<div class="flex items-center">
|
|
<img :src="player.avatar" class="h-10 w-10 rounded-full">
|
|
<div class="ml-3">
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white" x-text="player.name"></p>
|
|
<p class="text-xs text-gray-500" x-text="player.matches + ' matches'"></p>
|
|
</div>
|
|
</div>
|
|
<button @click="signPlayer(player.steam_id)" class="inline-flex items-center px-3 py-1 border border-transparent text-xs font-medium rounded text-yrtv-700 bg-yrtv-100 hover:bg-yrtv-200 dark:bg-yrtv-700 dark:text-white dark:hover:bg-yrtv-600">
|
|
Sign
|
|
</button>
|
|
</li>
|
|
</template>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
<div class="bg-gray-50 dark:bg-slate-700 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
|
|
<button type="button" @click="showScoutModal = false" class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm dark:bg-slate-600 dark:text-white dark:border-slate-500">
|
|
Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
function clubhouse() {
|
|
return {
|
|
team: {},
|
|
roster: [],
|
|
currentSort: 'rating', // Default sort
|
|
showScoutModal: false,
|
|
searchQuery: '',
|
|
searchResults: [],
|
|
|
|
init() {
|
|
this.fetchRoster();
|
|
},
|
|
|
|
fetchRoster() {
|
|
fetch('/teams/api/roster')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
this.team = data.team;
|
|
this.roster = data.roster;
|
|
this.sortRoster(); // Apply default sort
|
|
});
|
|
},
|
|
|
|
sortBy(key) {
|
|
this.currentSort = key;
|
|
this.sortRoster();
|
|
},
|
|
|
|
sortRoster() {
|
|
if (!this.roster || this.roster.length === 0) return;
|
|
|
|
this.roster.sort((a, b) => {
|
|
let valA = 0, valB = 0;
|
|
|
|
if (this.currentSort === 'rating') {
|
|
valA = a.stats?.basic_avg_rating || 0;
|
|
valB = b.stats?.basic_avg_rating || 0;
|
|
} else if (this.currentSort === 'kd') {
|
|
valA = a.stats?.basic_avg_kd || 0;
|
|
valB = b.stats?.basic_avg_kd || 0;
|
|
} else if (this.currentSort === 'matches') {
|
|
// matches_played is usually on the player object now? or stats?
|
|
// Check API: it's not explicitly in 'stats', but search added it.
|
|
// Roster API usually doesn't attach matches_played unless we ask.
|
|
// Let's assume stats.total_matches or check object root.
|
|
// Looking at roster API: we attach match counts? No, only search.
|
|
// But we can use total_matches from stats.
|
|
valA = a.stats?.total_matches || 0;
|
|
valB = b.stats?.total_matches || 0;
|
|
}
|
|
|
|
return valB - valA; // Descending
|
|
});
|
|
},
|
|
|
|
searchPlayers() {
|
|
if (this.searchQuery.length < 2) {
|
|
this.searchResults = [];
|
|
return;
|
|
}
|
|
// Use encodeURIComponent for safety
|
|
const q = encodeURIComponent(this.searchQuery);
|
|
console.log(`Searching for: ${q}`); // Debug Log
|
|
|
|
fetch(`/teams/api/search?q=${q}&sort=matches`)
|
|
.then(res => {
|
|
console.log('Response status:', res.status);
|
|
const contentType = res.headers.get("content-type");
|
|
if (contentType && contentType.indexOf("application/json") !== -1) {
|
|
return res.json();
|
|
} else {
|
|
// Not JSON, probably HTML error page
|
|
return res.text().then(text => {
|
|
console.error("Non-JSON response:", text.substring(0, 500));
|
|
throw new Error("Server returned non-JSON response");
|
|
});
|
|
}
|
|
})
|
|
.then(data => {
|
|
console.log('Search results:', data); // Debug Log
|
|
this.searchResults = data;
|
|
})
|
|
.catch(err => console.error('Search error:', err));
|
|
},
|
|
|
|
signPlayer(steamId) {
|
|
fetch('/teams/api/roster', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'add', steam_id: steamId })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
this.showScoutModal = false;
|
|
this.searchQuery = '';
|
|
this.searchResults = [];
|
|
this.fetchRoster(); // Refresh
|
|
});
|
|
},
|
|
|
|
removePlayer(steamId) {
|
|
if(!confirm('Are you sure you want to release this player?')) return;
|
|
|
|
fetch('/teams/api/roster', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ action: 'remove', steam_id: steamId })
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
this.fetchRoster();
|
|
});
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
{% endblock %}
|