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
|
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
|
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
|
➕ Add Player: http://localhost:3000/players/new
|
||||||
🎾 Record Match: http://localhost:3000/matches/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,
|
email: String,
|
||||||
rating_display: String,
|
rating_display: String,
|
||||||
has_email: bool,
|
has_email: bool,
|
||||||
|
wins: i64,
|
||||||
|
losses: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template, Clone, Debug)]
|
#[derive(Template, Clone, Debug)]
|
||||||
@ -1578,16 +1580,30 @@ async fn create_match(
|
|||||||
///
|
///
|
||||||
/// **Returns:** HTML page with players table
|
/// **Returns:** HTML page with players table
|
||||||
async fn players_list_handler(State(state): State<AppState>) -> impl IntoResponse {
|
async fn players_list_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
// UNIFIED RATING: use rating field (merged singles/doubles)
|
// UNIFIED RATING: use rating field (merged singles/doubles) with W-L record
|
||||||
let players: Vec<(i64, String, Option<String>, f64)> = sqlx::query_as(
|
let players: Vec<(i64, String, Option<String>, f64, i64, i64)> = sqlx::query_as(
|
||||||
"SELECT id, name, email, rating FROM players ORDER BY rating DESC"
|
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)
|
.fetch_all(&state.pool)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let players = players.into_iter()
|
let players = players.into_iter()
|
||||||
.map(|(id, name, email, rating)| {
|
.map(|(id, name, email, rating, wins, losses)| {
|
||||||
let has_email = email.is_some();
|
let has_email = email.is_some();
|
||||||
PlayerData {
|
PlayerData {
|
||||||
id,
|
id,
|
||||||
@ -1597,6 +1613,8 @@ async fn players_list_handler(State(state): State<AppState>) -> impl IntoRespons
|
|||||||
email: email.unwrap_or_default(),
|
email: email.unwrap_or_default(),
|
||||||
rating_display: format!("{:.1}", rating),
|
rating_display: format!("{:.1}", rating),
|
||||||
has_email,
|
has_email,
|
||||||
|
wins,
|
||||||
|
losses,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -1614,11 +1632,22 @@ async fn players_list_handler(State(state): State<AppState>) -> impl IntoRespons
|
|||||||
///
|
///
|
||||||
/// **Returns:** HTML page with unified leaderboard
|
/// **Returns:** HTML page with unified leaderboard
|
||||||
async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse {
|
async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
// UNIFIED RATING: Single leaderboard using rating field
|
// UNIFIED RATING: Single leaderboard using rating field with W-L record
|
||||||
let players: Vec<(i64, String, f64, Option<String>)> = sqlx::query_as(
|
let players: Vec<(i64, String, f64, Option<String>, i64, i64)> = sqlx::query_as(
|
||||||
r#"SELECT DISTINCT p.id, p.name, p.rating, p.email
|
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
|
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"#
|
ORDER BY p.rating DESC"#
|
||||||
)
|
)
|
||||||
.fetch_all(&state.pool)
|
.fetch_all(&state.pool)
|
||||||
@ -1626,7 +1655,7 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
let leaderboard = players.into_iter().enumerate()
|
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 has_email = email.is_some();
|
||||||
let player_data = PlayerData {
|
let player_data = PlayerData {
|
||||||
id,
|
id,
|
||||||
@ -1636,6 +1665,8 @@ async fn leaderboard_handler(State(state): State<AppState>) -> impl IntoResponse
|
|||||||
email: email.unwrap_or_default(),
|
email: email.unwrap_or_default(),
|
||||||
rating_display: format!("{:.1}", rating),
|
rating_display: format!("{:.1}", rating),
|
||||||
has_email,
|
has_email,
|
||||||
|
wins,
|
||||||
|
losses,
|
||||||
};
|
};
|
||||||
((i + 1) as i32, player_data)
|
((i + 1) as i32, player_data)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,12 +2,23 @@
|
|||||||
<h3 class="pitt-primary font-bold text-xl mb-2">
|
<h3 class="pitt-primary font-bold text-xl mb-2">
|
||||||
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
|
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="text-3xl font-bold pitt-primary mb-4">{{ player.singles_rating | round(1) }}</div>
|
<div class="grid grid-cols-2 gap-4 mb-4">
|
||||||
{% if player.email %}
|
<div>
|
||||||
<p class="text-sm text-gray-600">📧 {{ player.email }}</p>
|
<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 %}
|
{% endif %}
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2">
|
||||||
<a href="/players/{{ player.id }}" class="btn-primary text-sm">View Profile</a>
|
<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">Edit</a>
|
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm flex-1">Edit</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -17,11 +17,17 @@
|
|||||||
<a href="/players/{{ player.id }}/edit" class="btn-warning">✏️ Edit</a>
|
<a href="/players/{{ player.id }}/edit" class="btn-warning">✏️ Edit</a>
|
||||||
</div>
|
</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="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-sm text-gray-600 mb-1">ELO Rating</div>
|
||||||
<div class="text-4xl font-bold pitt-primary">{{ player.rating_display }}</div>
|
<div class="text-4xl font-bold pitt-primary">{{ player.rating_display }}</div>
|
||||||
</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="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-sm text-gray-600 mb-1">Matches Played</div>
|
||||||
<div class="text-4xl font-bold text-green-700">{{ match_count }}</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">
|
<h3 class="pitt-primary font-bold text-xl mb-2">
|
||||||
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
|
<a href="/players/{{ player.id }}" class="hover:underline">{{ player.name }}</a>
|
||||||
</h3>
|
</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 %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="flex gap-2 mt-4">
|
<div class="flex gap-2">
|
||||||
<a href="/players/{{ player.id }}" class="btn-primary text-sm">View Profile</a>
|
<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">Edit</a>
|
<a href="/players/{{ player.id }}/edit" class="btn-warning text-sm flex-1">Edit</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user