Add W-L records to player cards and leaderboard
This commit is contained in:
parent
1b74470fcb
commit
a1f96b9af4
@ -720,3 +720,4 @@ called `Result::unwrap()` on an `Err` value: Os { code: 48, kind: AddrInUse, mes
|
||||
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
|
||||
Migration error: error returned from database: (code: 1) no such column: rating
|
||||
Migration error: error returned from database: (code: 1) no such column: rating
|
||||
Migration error: error returned from database: (code: 1) no such column: rating
|
||||
|
||||
@ -1081,3 +1081,16 @@ Starting Pickleball ELO Tracker Server on port 3000...
|
||||
➕ Add Player: http://localhost:3000/players/new
|
||||
🎾 Record Match: http://localhost:3000/matches/new
|
||||
|
||||
🏓 Pickleball ELO Tracker v3.0
|
||||
==============================
|
||||
|
||||
Starting Pickleball ELO Tracker Server on port 3000...
|
||||
|
||||
✅ Server running at http://localhost:3000
|
||||
📊 Leaderboard: http://localhost:3000/leaderboard
|
||||
📜 Match History: http://localhost:3000/matches
|
||||
👥 Players: http://localhost:3000/players
|
||||
⚖️ Team Balancer: http://localhost:3000/balance
|
||||
➕ Add Player: http://localhost:3000/players/new
|
||||
🎾 Record Match: http://localhost:3000/matches/new
|
||||
|
||||
|
||||
BIN
pickleball-elo
BIN
pickleball-elo
Binary file not shown.
49
src/main.rs
49
src/main.rs
@ -205,6 +205,8 @@ struct PlayerData {
|
||||
email: String,
|
||||
rating_display: String,
|
||||
has_email: bool,
|
||||
wins: i64,
|
||||
losses: i64,
|
||||
}
|
||||
|
||||
#[derive(Template, Clone, Debug)]
|
||||
@ -1578,16 +1580,30 @@ async fn create_match(
|
||||
///
|
||||
/// **Returns:** HTML page with players table
|
||||
async fn players_list_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
// UNIFIED RATING: use rating field (merged singles/doubles)
|
||||
let players: Vec<(i64, String, Option<String>, f64)> = sqlx::query_as(
|
||||
"SELECT id, name, email, rating FROM players ORDER BY rating DESC"
|
||||
// UNIFIED RATING: use rating field (merged singles/doubles) with W-L record
|
||||
let players: Vec<(i64, String, Option<String>, f64, i64, i64)> = sqlx::query_as(
|
||||
r#"SELECT
|
||||
p.id, p.name, p.email, p.rating,
|
||||
COALESCE(SUM(CASE WHEN
|
||||
(mp.team = 1 AND m.team1_score > m.team2_score) OR
|
||||
(mp.team = 2 AND m.team2_score > m.team1_score)
|
||||
THEN 1 ELSE 0 END), 0) as wins,
|
||||
COALESCE(SUM(CASE WHEN
|
||||
(mp.team = 1 AND m.team1_score < m.team2_score) OR
|
||||
(mp.team = 2 AND m.team2_score < m.team1_score)
|
||||
THEN 1 ELSE 0 END), 0) as losses
|
||||
FROM players p
|
||||
LEFT JOIN match_participants mp ON p.id = mp.player_id
|
||||
LEFT JOIN matches m ON mp.match_id = m.id
|
||||
GROUP BY p.id
|
||||
ORDER BY p.rating DESC"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let players = players.into_iter()
|
||||
.map(|(id, name, email, rating)| {
|
||||
.map(|(id, name, email, rating, wins, losses)| {
|
||||
let has_email = email.is_some();
|
||||
PlayerData {
|
||||
id,
|
||||
@ -1597,6 +1613,8 @@ async fn players_list_handler(State(state): State<AppState>) -> impl IntoRespons
|
||||
email: email.unwrap_or_default(),
|
||||
rating_display: format!("{:.1}", rating),
|
||||
has_email,
|
||||
wins,
|
||||
losses,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
@ -1614,11 +1632,22 @@ async fn players_list_handler(State(state): State<AppState>) -> impl IntoRespons
|
||||
///
|
||||
/// **Returns:** HTML page with unified leaderboard
|
||||
async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||
// UNIFIED RATING: Single leaderboard using rating field
|
||||
let players: Vec<(i64, String, f64, Option<String>)> = sqlx::query_as(
|
||||
r#"SELECT DISTINCT p.id, p.name, p.rating, p.email
|
||||
// UNIFIED RATING: Single leaderboard using rating field with W-L record
|
||||
let players: Vec<(i64, String, f64, Option<String>, i64, i64)> = sqlx::query_as(
|
||||
r#"SELECT
|
||||
p.id, p.name, p.rating, p.email,
|
||||
COALESCE(SUM(CASE WHEN
|
||||
(mp.team = 1 AND m.team1_score > m.team2_score) OR
|
||||
(mp.team = 2 AND m.team2_score > m.team1_score)
|
||||
THEN 1 ELSE 0 END), 0) as wins,
|
||||
COALESCE(SUM(CASE WHEN
|
||||
(mp.team = 1 AND m.team1_score < m.team2_score) OR
|
||||
(mp.team = 2 AND m.team2_score < m.team1_score)
|
||||
THEN 1 ELSE 0 END), 0) as losses
|
||||
FROM players p
|
||||
JOIN match_participants mp ON p.id = mp.player_id
|
||||
LEFT JOIN match_participants mp ON p.id = mp.player_id
|
||||
LEFT JOIN matches m ON mp.match_id = m.id
|
||||
GROUP BY p.id
|
||||
ORDER BY p.rating DESC"#
|
||||
)
|
||||
.fetch_all(&state.pool)
|
||||
@ -1626,7 +1655,7 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
|
||||
.unwrap_or_default();
|
||||
|
||||
let leaderboard = players.into_iter().enumerate()
|
||||
.map(|(i, (id, name, rating, email))| {
|
||||
.map(|(i, (id, name, rating, email, wins, losses))| {
|
||||
let has_email = email.is_some();
|
||||
let player_data = PlayerData {
|
||||
id,
|
||||
@ -1636,6 +1665,8 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
|
||||
email: email.unwrap_or_default(),
|
||||
rating_display: format!("{:.1}", rating),
|
||||
has_email,
|
||||
wins,
|
||||
losses,
|
||||
};
|
||||
((i + 1) as i32, player_data)
|
||||
})
|
||||
|
||||
@ -2,12 +2,23 @@
|
||||
<h3 class="pitt-primary font-bold text-xl mb-2">
|
||||
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
|
||||
</h3>
|
||||
<div class="text-3xl font-bold pitt-primary mb-4">{{ player.singles_rating | round(1) }}</div>
|
||||
{% if player.email %}
|
||||
<p class="text-sm text-gray-600">📧 {{ player.email }}</p>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 uppercase">Rating</div>
|
||||
<div class="text-2xl font-bold pitt-primary">{{ player.rating_display }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 uppercase">Record</div>
|
||||
<div class="text-2xl font-bold">
|
||||
<span class="text-green-600">{{ player.wins }}</span>-<span class="text-red-600">{{ player.losses }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if player.has_email %}
|
||||
<p class="text-sm text-gray-600 mb-4">📧 {{ player.email }}</p>
|
||||
{% endif %}
|
||||
<div class="flex gap-2 mt-4">
|
||||
<a href="/players/{{ player.id }}" class="btn-primary text-sm">View Profile</a>
|
||||
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm">Edit</a>
|
||||
<div class="flex gap-2">
|
||||
<a href="/players/{{ player.id }}" class="btn-primary text-sm flex-1">View Profile</a>
|
||||
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm flex-1">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -17,11 +17,17 @@
|
||||
<a href="/players/{{ player.id }}/edit" class="btn-warning">✏️ Edit</a>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div class="bg-gradient-to-br from-blue-50 to-blue-100 p-6 rounded-lg">
|
||||
<div class="text-sm text-gray-600 mb-1">ELO Rating</div>
|
||||
<div class="text-4xl font-bold pitt-primary">{{ player.rating_display }}</div>
|
||||
</div>
|
||||
<div class="bg-gradient-to-br from-purple-50 to-purple-100 p-6 rounded-lg">
|
||||
<div class="text-sm text-gray-600 mb-1">Record</div>
|
||||
<div class="text-4xl font-bold">
|
||||
<span class="text-green-600">{{ player.wins }}</span><span class="text-gray-400">-</span><span class="text-red-600">{{ player.losses }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-gradient-to-br from-green-50 to-green-100 p-6 rounded-lg">
|
||||
<div class="text-sm text-gray-600 mb-1">Matches Played</div>
|
||||
<div class="text-4xl font-bold text-green-700">{{ match_count }}</div>
|
||||
|
||||
@ -18,13 +18,24 @@
|
||||
<h3 class="pitt-primary font-bold text-xl mb-2">
|
||||
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
|
||||
</h3>
|
||||
<div class="text-3xl font-bold pitt-primary mb-4">{{ player.rating_display }}</div>
|
||||
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 uppercase">Rating</div>
|
||||
<div class="text-2xl font-bold pitt-primary">{{ player.rating_display }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-gray-500 uppercase">Record</div>
|
||||
<div class="text-2xl font-bold">
|
||||
<span class="text-green-600">{{ player.wins }}</span>-<span class="text-red-600">{{ player.losses }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if player.has_email %}
|
||||
<p class="text-sm text-gray-600">📧 {{ player.email }}</p>
|
||||
<p class="text-sm text-gray-600 mb-4">📧 {{ player.email }}</p>
|
||||
{% endif %}
|
||||
<div class="flex gap-2 mt-4">
|
||||
<a href="/players/{{ player.id }}" class="btn-primary text-sm">View Profile</a>
|
||||
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm">Edit</a>
|
||||
<div class="flex gap-2">
|
||||
<a href="/players/{{ player.id }}" class="btn-primary text-sm flex-1">View Profile</a>
|
||||
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm flex-1">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user