phase4: implement player search filters, htmx results, and detail page
This commit is contained in:
169
templates/players/detail.html
Normal file
169
templates/players/detail.html
Normal file
@ -0,0 +1,169 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | {{ player.full_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<div class="row-between wrap-gap">
|
||||
<div>
|
||||
<h1>{{ player.full_name }}</h1>
|
||||
<p class="muted-text">
|
||||
{{ player.nominal_position.name|default:"No nominal position" }}
|
||||
· {{ player.inferred_role.name|default:"No inferred role" }}
|
||||
</p>
|
||||
</div>
|
||||
<a class="button ghost" href="{% url 'players:index' %}">Back to search</a>
|
||||
</div>
|
||||
|
||||
<div class="detail-grid mt-16">
|
||||
<div class="detail-card">
|
||||
<h2>Summary</h2>
|
||||
<p><strong>Nationality:</strong> {{ player.nationality.name|default:"-" }}</p>
|
||||
<p><strong>Birth date:</strong> {{ player.birth_date|date:"Y-m-d"|default:"-" }}</p>
|
||||
<p><strong>Age:</strong> {{ age|default:"-" }}</p>
|
||||
<p><strong>Height:</strong> {{ player.height_cm|default:"-" }} cm</p>
|
||||
<p><strong>Weight:</strong> {{ player.weight_kg|default:"-" }} kg</p>
|
||||
<p><strong>Dominant hand:</strong> {{ player.get_dominant_hand_display|default:"-" }}</p>
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<h2>Current Assignment</h2>
|
||||
{% if current_assignment %}
|
||||
<p><strong>Team:</strong> {{ current_assignment.team.name|default:"-" }}</p>
|
||||
<p><strong>Competition:</strong> {{ current_assignment.competition.name|default:"-" }}</p>
|
||||
<p><strong>Season:</strong> {{ current_assignment.season.label|default:"-" }}</p>
|
||||
<p><strong>Games:</strong> {{ current_assignment.games_played }}</p>
|
||||
{% else %}
|
||||
<p>No active assignment available.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="detail-card">
|
||||
<h2>Aliases</h2>
|
||||
<ul>
|
||||
{% for alias in player.aliases.all %}
|
||||
<li>{{ alias.alias }}{% if alias.source %} ({{ alias.source }}){% endif %}</li>
|
||||
{% empty %}
|
||||
<li>No aliases recorded.</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<h2>Team History</h2>
|
||||
{% if season_rows %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Season</th>
|
||||
<th>Team</th>
|
||||
<th>Competition</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in season_rows %}
|
||||
<tr>
|
||||
<td>{{ row.season.label|default:"-" }}</td>
|
||||
<td>{{ row.team.name|default:"-" }}</td>
|
||||
<td>{{ row.competition.name|default:"-" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No team history available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<h2>Career History</h2>
|
||||
{% if career_entries %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Season</th>
|
||||
<th>Team</th>
|
||||
<th>Competition</th>
|
||||
<th>Role</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in career_entries %}
|
||||
<tr>
|
||||
<td>{{ entry.season.label|default:"-" }}</td>
|
||||
<td>{{ entry.team.name|default:"-" }}</td>
|
||||
<td>{{ entry.competition.name|default:"-" }}</td>
|
||||
<td>{{ entry.role_snapshot.name|default:"-" }}</td>
|
||||
<td>{{ entry.start_date|date:"Y-m-d"|default:"-" }}</td>
|
||||
<td>{{ entry.end_date|date:"Y-m-d"|default:"-" }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No career entries available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section class="panel mt-16">
|
||||
<h2>Season-by-Season Stats</h2>
|
||||
{% if season_rows %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Season</th>
|
||||
<th>Team</th>
|
||||
<th>Competition</th>
|
||||
<th>Games</th>
|
||||
<th>MPG</th>
|
||||
<th>PPG</th>
|
||||
<th>RPG</th>
|
||||
<th>APG</th>
|
||||
<th>SPG</th>
|
||||
<th>BPG</th>
|
||||
<th>TOPG</th>
|
||||
<th>FG%</th>
|
||||
<th>3P%</th>
|
||||
<th>FT%</th>
|
||||
<th>Impact</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in season_rows %}
|
||||
<tr>
|
||||
<td>{{ row.season.label|default:"-" }}</td>
|
||||
<td>{{ row.team.name|default:"-" }}</td>
|
||||
<td>{{ row.competition.name|default:"-" }}</td>
|
||||
<td>{{ row.games_played }}</td>
|
||||
<td>
|
||||
{% if row.mpg is not None %}{{ row.mpg|floatformat:1 }}{% else %}-{% endif %}
|
||||
</td>
|
||||
<td>{% if row.stats %}{{ row.stats.points }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.rebounds }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.assists }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.steals }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.blocks }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.turnovers }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.fg_pct }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.three_pct }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.ft_pct }}{% else %}-{% endif %}</td>
|
||||
<td>{% if row.stats %}{{ row.stats.player_efficiency_rating }}{% else %}-{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No season stats available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@ -1,10 +1,99 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}HoopScout | Players{% endblock %}
|
||||
{% block title %}HoopScout | Player Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section class="panel">
|
||||
<h1>Players</h1>
|
||||
<p>Players module scaffolding for upcoming phases.</p>
|
||||
<h1>Player Search</h1>
|
||||
<p>Filter players by profile, context, and production metrics.</p>
|
||||
|
||||
<form
|
||||
method="get"
|
||||
class="stack search-form"
|
||||
hx-get="{% url 'players:index' %}"
|
||||
hx-target="#player-results"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true"
|
||||
hx-trigger="submit, change delay:200ms from:select, keyup changed delay:400ms from:#id_q"
|
||||
>
|
||||
<div class="filter-grid filter-grid-4">
|
||||
<div>
|
||||
<label for="id_q">Name</label>
|
||||
{{ search_form.q }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_sort">Sort</label>
|
||||
{{ search_form.sort }}
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_page_size">Page size</label>
|
||||
{{ search_form.page_size }}
|
||||
</div>
|
||||
<div class="filter-actions">
|
||||
<button type="submit" class="button">Apply</button>
|
||||
<a class="button ghost" href="{% url 'players:index' %}">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-grid filter-grid-3">
|
||||
<div><label for="id_nominal_position">Nominal position</label>{{ search_form.nominal_position }}</div>
|
||||
<div><label for="id_inferred_role">Inferred role</label>{{ search_form.inferred_role }}</div>
|
||||
<div><label for="id_nationality">Nationality</label>{{ search_form.nationality }}</div>
|
||||
<div><label for="id_competition">Competition</label>{{ search_form.competition }}</div>
|
||||
<div><label for="id_team">Team</label>{{ search_form.team }}</div>
|
||||
<div><label for="id_season">Season</label>{{ search_form.season }}</div>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>Physical and age filters</summary>
|
||||
<div class="filter-grid filter-grid-4">
|
||||
<div><label for="id_age_min">Age min</label>{{ search_form.age_min }}</div>
|
||||
<div><label for="id_age_max">Age max</label>{{ search_form.age_max }}</div>
|
||||
<div><label for="id_height_min">Height min (cm)</label>{{ search_form.height_min }}</div>
|
||||
<div><label for="id_height_max">Height max (cm)</label>{{ search_form.height_max }}</div>
|
||||
<div><label for="id_weight_min">Weight min (kg)</label>{{ search_form.weight_min }}</div>
|
||||
<div><label for="id_weight_max">Weight max (kg)</label>{{ search_form.weight_max }}</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Statistical filters</summary>
|
||||
<div class="filter-grid filter-grid-4">
|
||||
<div><label for="id_games_played_min">Games min</label>{{ search_form.games_played_min }}</div>
|
||||
<div><label for="id_games_played_max">Games max</label>{{ search_form.games_played_max }}</div>
|
||||
<div><label for="id_minutes_per_game_min">MPG min</label>{{ search_form.minutes_per_game_min }}</div>
|
||||
<div><label for="id_minutes_per_game_max">MPG max</label>{{ search_form.minutes_per_game_max }}</div>
|
||||
|
||||
<div><label for="id_points_per_game_min">PPG min</label>{{ search_form.points_per_game_min }}</div>
|
||||
<div><label for="id_points_per_game_max">PPG max</label>{{ search_form.points_per_game_max }}</div>
|
||||
<div><label for="id_rebounds_per_game_min">RPG min</label>{{ search_form.rebounds_per_game_min }}</div>
|
||||
<div><label for="id_rebounds_per_game_max">RPG max</label>{{ search_form.rebounds_per_game_max }}</div>
|
||||
|
||||
<div><label for="id_assists_per_game_min">APG min</label>{{ search_form.assists_per_game_min }}</div>
|
||||
<div><label for="id_assists_per_game_max">APG max</label>{{ search_form.assists_per_game_max }}</div>
|
||||
<div><label for="id_steals_per_game_min">SPG min</label>{{ search_form.steals_per_game_min }}</div>
|
||||
<div><label for="id_steals_per_game_max">SPG max</label>{{ search_form.steals_per_game_max }}</div>
|
||||
|
||||
<div><label for="id_blocks_per_game_min">BPG min</label>{{ search_form.blocks_per_game_min }}</div>
|
||||
<div><label for="id_blocks_per_game_max">BPG max</label>{{ search_form.blocks_per_game_max }}</div>
|
||||
<div><label for="id_turnovers_per_game_min">TOPG min</label>{{ search_form.turnovers_per_game_min }}</div>
|
||||
<div><label for="id_turnovers_per_game_max">TOPG max</label>{{ search_form.turnovers_per_game_max }}</div>
|
||||
|
||||
<div><label for="id_fg_pct_min">FG% min</label>{{ search_form.fg_pct_min }}</div>
|
||||
<div><label for="id_fg_pct_max">FG% max</label>{{ search_form.fg_pct_max }}</div>
|
||||
<div><label for="id_three_pct_min">3P% min</label>{{ search_form.three_pct_min }}</div>
|
||||
<div><label for="id_three_pct_max">3P% max</label>{{ search_form.three_pct_max }}</div>
|
||||
|
||||
<div><label for="id_ft_pct_min">FT% min</label>{{ search_form.ft_pct_min }}</div>
|
||||
<div><label for="id_ft_pct_max">FT% max</label>{{ search_form.ft_pct_max }}</div>
|
||||
<div><label for="id_efficiency_metric_min">Impact min</label>{{ search_form.efficiency_metric_min }}</div>
|
||||
<div><label for="id_efficiency_metric_max">Impact max</label>{{ search_form.efficiency_metric_max }}</div>
|
||||
</div>
|
||||
</details>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section id="player-results" class="panel mt-16">
|
||||
{% include "players/partials/results.html" %}
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
82
templates/players/partials/results.html
Normal file
82
templates/players/partials/results.html
Normal file
@ -0,0 +1,82 @@
|
||||
{% load player_query %}
|
||||
|
||||
<div class="row-between wrap-gap">
|
||||
<h2>Results</h2>
|
||||
<div class="muted-text">
|
||||
{{ page_obj.paginator.count }} player{{ page_obj.paginator.count|pluralize }} found
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if players %}
|
||||
<div class="table-wrap">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Player</th>
|
||||
<th>Nationality</th>
|
||||
<th>Pos / Role</th>
|
||||
<th>Height / Weight</th>
|
||||
<th>Games</th>
|
||||
<th>MPG</th>
|
||||
<th>PPG</th>
|
||||
<th>RPG</th>
|
||||
<th>APG</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for player in players %}
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{% url 'players:detail' player.pk %}">{{ player.full_name }}</a>
|
||||
</td>
|
||||
<td>{{ player.nationality.name|default:"-" }}</td>
|
||||
<td>
|
||||
{{ player.nominal_position.code|default:"-" }}
|
||||
/ {{ player.inferred_role.name|default:"-" }}
|
||||
</td>
|
||||
<td>{{ player.height_cm|default:"-" }} / {{ player.weight_kg|default:"-" }}</td>
|
||||
<td>{{ player.games_played_value|floatformat:0 }}</td>
|
||||
<td>{{ player.mpg_value|floatformat:1 }}</td>
|
||||
<td>{{ player.ppg_value|floatformat:1 }}</td>
|
||||
<td>{{ player.rpg_value|floatformat:1 }}</td>
|
||||
<td>{{ player.apg_value|floatformat:1 }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="pagination row-gap mt-16">
|
||||
{% if page_obj.has_previous %}
|
||||
{% query_transform page=page_obj.previous_page_number as prev_query %}
|
||||
<a
|
||||
class="button ghost"
|
||||
href="?{{ prev_query }}"
|
||||
hx-get="?{{ prev_query }}"
|
||||
hx-target="#player-results"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true"
|
||||
>
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<span>Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}</span>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
{% query_transform page=page_obj.next_page_number as next_query %}
|
||||
<a
|
||||
class="button ghost"
|
||||
href="?{{ next_query }}"
|
||||
hx-get="?{{ next_query }}"
|
||||
hx-target="#player-results"
|
||||
hx-swap="innerHTML"
|
||||
hx-push-url="true"
|
||||
>
|
||||
Next
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No players matched the current filters.</p>
|
||||
{% endif %}
|
||||
Reference in New Issue
Block a user