feat(players): add player profile and schedule views
Implement player-facing views for profile analytics and tournament scheduling. Changes: - Add player profile page with partnership statistics - Add player schedule page with upcoming matches - Update rankings page to link to player profiles - Add partnership tracking display in profile - Include recent games timeline in profile Note: Schedule view shows upcoming matches and tournament participation
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Players
|
||||||
|
class Profile < EuchreCamp::Action
|
||||||
|
include Deps[players_repo: 'repositories.players']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
player_id = request.params[:id]
|
||||||
|
player = players_repo.by_id(player_id)
|
||||||
|
|
||||||
|
# Get partnership statistics
|
||||||
|
partnerships = EuchreCamp::Services::PartnershipTracker.get_player_partnerships(player_id)
|
||||||
|
|
||||||
|
# Get recent games (last 10)
|
||||||
|
recent_games = get_recent_games(player_id)
|
||||||
|
|
||||||
|
response.render(view,
|
||||||
|
player: player,
|
||||||
|
partnerships: partnerships,
|
||||||
|
recent_games: recent_games
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_recent_games(player_id)
|
||||||
|
rom = EuchreCamp::App["persistence.rom"]
|
||||||
|
|
||||||
|
# Get matches where this player participated
|
||||||
|
matches = rom.relations[:matches]
|
||||||
|
.where(
|
||||||
|
Sequel.|(
|
||||||
|
{ team_1_p1_id: player_id },
|
||||||
|
{ team_1_p2_id: player_id },
|
||||||
|
{ team_2_p1_id: player_id },
|
||||||
|
{ team_2_p2_id: player_id }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.order(Sequel.desc(:played_at))
|
||||||
|
.limit(10)
|
||||||
|
.to_a
|
||||||
|
|
||||||
|
# Get player names for display
|
||||||
|
all_players = rom.relations[:players].to_a.to_h { |p| [p[:id], p] }
|
||||||
|
|
||||||
|
matches.map do |match|
|
||||||
|
team_1_players = [
|
||||||
|
all_players[match[:team_1_p1_id]],
|
||||||
|
all_players[match[:team_1_p2_id]]
|
||||||
|
]
|
||||||
|
team_2_players = [
|
||||||
|
all_players[match[:team_2_p1_id]],
|
||||||
|
all_players[match[:team_2_p2_id]]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Determine if this player won
|
||||||
|
player_on_team_1 = match[:team_1_p1_id] == player_id || match[:team_1_p2_id] == player_id
|
||||||
|
won = player_on_team_1 ? (match[:team_1_score] > match[:team_2_score]) : (match[:team_2_score] > match[:team_1_score])
|
||||||
|
|
||||||
|
{
|
||||||
|
date: match[:played_at] || match[:created_at],
|
||||||
|
team_1: team_1_players,
|
||||||
|
team_2: team_2_players,
|
||||||
|
score: "#{match[:team_1_score]}-#{match[:team_2_score]}",
|
||||||
|
won: won,
|
||||||
|
player_partner: player_on_team_1 ? team_1_players.find { |p| p[:id] != player_id } : team_2_players.find { |p| p[:id] != player_id }
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module EuchreCamp
|
||||||
|
module Actions
|
||||||
|
module Players
|
||||||
|
class Schedule < EuchreCamp::Action
|
||||||
|
include Deps[players_repo: 'repositories.players']
|
||||||
|
|
||||||
|
def handle(request, response)
|
||||||
|
player_id = request.params[:id]
|
||||||
|
player = players_repo.by_id(player_id)
|
||||||
|
|
||||||
|
# Get upcoming matches for this player
|
||||||
|
upcoming_matches = get_upcoming_matches(player_id)
|
||||||
|
|
||||||
|
# Get active tournaments for this player
|
||||||
|
active_tournaments = get_active_tournaments(player_id)
|
||||||
|
|
||||||
|
# Get tournament schedule
|
||||||
|
tournament_schedule = get_tournament_schedule(player_id)
|
||||||
|
|
||||||
|
response.render(view,
|
||||||
|
player: player,
|
||||||
|
upcoming_matches: upcoming_matches,
|
||||||
|
active_tournaments: active_tournaments,
|
||||||
|
tournament_schedule: tournament_schedule
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def get_upcoming_matches(player_id)
|
||||||
|
rom = EuchreCamp::App["persistence.rom"]
|
||||||
|
|
||||||
|
# Get upcoming matches where this player participates
|
||||||
|
matches = rom.relations[:matches]
|
||||||
|
.where(
|
||||||
|
Sequel.|(
|
||||||
|
{ team_1_p1_id: player_id },
|
||||||
|
{ team_1_p2_id: player_id },
|
||||||
|
{ team_2_p1_id: player_id },
|
||||||
|
{ team_2_p2_id: player_id }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(status: 'scheduled')
|
||||||
|
.order(:played_at)
|
||||||
|
.limit(10)
|
||||||
|
.to_a
|
||||||
|
|
||||||
|
# Get player names for display
|
||||||
|
all_players = rom.relations[:players].to_a.to_h { |p| [p[:id], p] }
|
||||||
|
|
||||||
|
matches.map do |match|
|
||||||
|
team_1_players = [
|
||||||
|
all_players[match[:team_1_p1_id]],
|
||||||
|
all_players[match[:team_1_p2_id]]
|
||||||
|
]
|
||||||
|
team_2_players = [
|
||||||
|
all_players[match[:team_2_p1_id]],
|
||||||
|
all_players[match[:team_2_p2_id]]
|
||||||
|
]
|
||||||
|
|
||||||
|
player_on_team_1 = match[:team_1_p1_id] == player_id || match[:team_1_p2_id] == player_id
|
||||||
|
|
||||||
|
{
|
||||||
|
id: match[:id],
|
||||||
|
date: match[:played_at],
|
||||||
|
team_1: team_1_players,
|
||||||
|
team_2: team_2_players,
|
||||||
|
opponent_team: player_on_team_1 ? team_2_players : team_1_players,
|
||||||
|
player_team: player_on_team_1 ? team_1_players : team_2_players,
|
||||||
|
event_id: match[:event_id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_active_tournaments(player_id)
|
||||||
|
rom = EuchreCamp::App["persistence.rom"]
|
||||||
|
|
||||||
|
# Get tournaments where player is participating
|
||||||
|
event_ids = rom.relations[:event_participants]
|
||||||
|
.where(player_id: player_id)
|
||||||
|
.select(:event_id)
|
||||||
|
.to_a
|
||||||
|
.map { |e| e[:event_id] }
|
||||||
|
|
||||||
|
return [] if event_ids.empty?
|
||||||
|
|
||||||
|
# Get active tournaments
|
||||||
|
rom.relations[:events]
|
||||||
|
.where(id: event_ids, status: 'active')
|
||||||
|
.to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_tournament_schedule(player_id)
|
||||||
|
rom = EuchreCamp::App["persistence.rom"]
|
||||||
|
|
||||||
|
# Get all tournaments where player participates
|
||||||
|
event_ids = rom.relations[:event_participants]
|
||||||
|
.where(player_id: player_id)
|
||||||
|
.select(:event_id)
|
||||||
|
.to_a
|
||||||
|
.map { |e| e[:event_id] }
|
||||||
|
|
||||||
|
return [] if event_ids.empty?
|
||||||
|
|
||||||
|
# Get tournaments and their schedules
|
||||||
|
tournaments = rom.relations[:events]
|
||||||
|
.where(id: event_ids)
|
||||||
|
.to_a
|
||||||
|
|
||||||
|
tournaments.map do |tournament|
|
||||||
|
# Get tournament rounds
|
||||||
|
rounds = rom.relations[:tournament_rounds]
|
||||||
|
.where(event_id: tournament[:id])
|
||||||
|
.order(:round_number)
|
||||||
|
.to_a
|
||||||
|
|
||||||
|
{
|
||||||
|
tournament: tournament,
|
||||||
|
rounds: rounds
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<h1>Player Profile: <%= player.name %></h1>
|
||||||
|
|
||||||
|
<div class="profile-header">
|
||||||
|
<div class="player-stats">
|
||||||
|
<div class="stat">
|
||||||
|
<span class="label">ELO Rating</span>
|
||||||
|
<span class="value"><%= player.current_elo %></span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="label">Total Games</span>
|
||||||
|
<span class="value"><%= recent_games.count %></span>
|
||||||
|
</div>
|
||||||
|
<div class="stat">
|
||||||
|
<span class="label">Win Rate</span>
|
||||||
|
<span class="value"><%= (recent_games.count > 0 ? (recent_games.count { |g| g[:won] }.to_f / recent_games.count * 100).round(1) : 0) %>%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Partnership Performance</h2>
|
||||||
|
|
||||||
|
<% if partnerships.any? %>
|
||||||
|
<div class="partnerships-table">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Partner</th>
|
||||||
|
<th>Games</th>
|
||||||
|
<th>Win Rate</th>
|
||||||
|
<th>ELO Change</th>
|
||||||
|
<th>Avg/Match</th>
|
||||||
|
<th>Last Played</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% partnerships.each do |p| %>
|
||||||
|
<tr>
|
||||||
|
<td><a href="/players/<%= p[:partner][:id] %>"><%= p[:partner][:name] %></a></td>
|
||||||
|
<td><%= p[:games_played] %></td>
|
||||||
|
<td>
|
||||||
|
<%= (p[:win_rate] * 100).round(1) %>%
|
||||||
|
<div class="confidence-<%= p[:games_played] >= 30 ? 'high' : p[:games_played] >= 15 ? 'medium' : 'low' %>">
|
||||||
|
<% if p[:games_played] < 15 %>
|
||||||
|
<small>(low confidence)</small>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td><%= p[:total_elo_change] > 0 ? "+" : "" %><%= p[:total_elo_change] %></td>
|
||||||
|
<td><%= p[:avg_elo_change] > 0 ? "+" : "" %><%= p[:avg_elo_change] %></td>
|
||||||
|
<td><%= p[:last_played]&.strftime('%Y-%m-%d') || '-' %></td>
|
||||||
|
</tr>
|
||||||
|
<% end %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p>No partnership data available yet.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Recent Games</h2>
|
||||||
|
|
||||||
|
<% if recent_games.any? %>
|
||||||
|
<div class="recent-games">
|
||||||
|
<% recent_games.each do |game| %>
|
||||||
|
<div class="game <%= game[:won] ? 'won' : 'lost' %>">
|
||||||
|
<div class="game-date">
|
||||||
|
<%= game[:date]&.strftime('%Y-%m-%d %H:%M') || 'N/A' %>
|
||||||
|
</div>
|
||||||
|
<div class="game-teams">
|
||||||
|
<div class="team team-1 <%= game[:won] ? 'winner' : '' %>">
|
||||||
|
<%= game[:team_1].map { |p| p[:name] }.join(' + ') %>
|
||||||
|
</div>
|
||||||
|
<div class="game-score">
|
||||||
|
<%= game[:score] %>
|
||||||
|
</div>
|
||||||
|
<div class="team team-2 <%= !game[:won] ? 'winner' : '' %>">
|
||||||
|
<%= game[:team_2].map { |p| p[:name] }.join(' + ') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="game-partner">
|
||||||
|
Partner: <%= game[:player_partner][:name] %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p>No games played yet.</p>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<p><a href="/rankings">Back to Rankings</a></p>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<h1><%= player.name %>'s Schedule</h1>
|
||||||
|
|
||||||
|
<div class="schedule-container">
|
||||||
|
<!-- Upcoming Matches Section -->
|
||||||
|
<section class="upcoming-matches">
|
||||||
|
<h2>Upcoming Matches</h2>
|
||||||
|
|
||||||
|
<% if upcoming_matches.any? %>
|
||||||
|
<div class="matches-list">
|
||||||
|
<% upcoming_matches.each do |match| %>
|
||||||
|
<div class="match-card">
|
||||||
|
<div class="match-date">
|
||||||
|
<%= match[:date]&.strftime('%a, %b %d') || 'TBD' %>
|
||||||
|
<%= match[:date]&.strftime('%I:%M %p') %>
|
||||||
|
</div>
|
||||||
|
<div class="match-details">
|
||||||
|
<div class="teams">
|
||||||
|
<div class="team your-team">
|
||||||
|
<%= match[:player_team].map { |p| p[:name] }.join(' + ') %>
|
||||||
|
</div>
|
||||||
|
<div class="vs">vs</div>
|
||||||
|
<div class="team opponent-team">
|
||||||
|
<%= match[:opponent_team].map { |p| p[:name] }.join(' + ') %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="match-actions">
|
||||||
|
<a href="/admin/matches/<%= match[:id] %>/edit" class="btn btn-small">Record Result</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="no-matches">No upcoming matches scheduled.</p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Active Tournaments Section -->
|
||||||
|
<section class="active-tournaments">
|
||||||
|
<h2>Active Tournaments</h2>
|
||||||
|
|
||||||
|
<% if active_tournaments.any? %>
|
||||||
|
<div class="tournaments-list">
|
||||||
|
<% active_tournaments.each do |tournament| %>
|
||||||
|
<div class="tournament-card">
|
||||||
|
<div class="tournament-info">
|
||||||
|
<h3><%= tournament[:name] %></h3>
|
||||||
|
<p class="tournament-format"><%= tournament[:format].capitalize %> Tournament</p>
|
||||||
|
</div>
|
||||||
|
<div class="tournament-status">
|
||||||
|
<span class="status-badge <%= tournament[:status] %>"><%= tournament[:status].capitalize %></span>
|
||||||
|
</div>
|
||||||
|
<div class="tournament-actions">
|
||||||
|
<a href="/admin/tournaments/<%= tournament[:id] %>" class="btn btn-small">View Details</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="no-tournaments">No active tournaments.</p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Tournament Schedule Section -->
|
||||||
|
<section class="tournament-schedule">
|
||||||
|
<h2>Tournament Schedule</h2>
|
||||||
|
|
||||||
|
<% if tournament_schedule.any? %>
|
||||||
|
<div class="schedule-by-tournament">
|
||||||
|
<% tournament_schedule.each do |schedule| %>
|
||||||
|
<div class="tournament-schedule-item">
|
||||||
|
<h3><%= schedule[:tournament][:name] %></h3>
|
||||||
|
<div class="rounds">
|
||||||
|
<% schedule[:rounds].each do |round| %>
|
||||||
|
<div class="round">
|
||||||
|
<span class="round-number">Round <%= round[:round_number] %></span>
|
||||||
|
<span class="round-status"><%= round[:status] %></span>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
<% else %>
|
||||||
|
<p class="no-schedule">No tournament schedule available.</p>
|
||||||
|
<% end %>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p><a href="/players/<%= player.id %>/profile">Back to Profile</a></p>
|
||||||
Reference in New Issue
Block a user