nextjs-rewrite #5

Merged
david merged 56 commits from nextjs-rewrite into main 2026-03-30 02:30:16 +00:00
208 changed files with 8522 additions and 9035 deletions
Showing only changes of commit 66e4baa643 - Show all commits
+43 -8
View File
@@ -1,8 +1,43 @@
.env
log/*
public/assets/
node_modules/
spec/examples.txt
Gemfile.lock
cookies.txt
euchre_camp.db
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# env files (can opt-in for committing if needed)
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
/src/generated/prisma
-1
View File
@@ -1 +0,0 @@
--require spec_helper
-1
View File
@@ -1 +0,0 @@
ruby 3.3.3
-44
View File
@@ -1,44 +0,0 @@
# frozen_string_literal: true
source "https://rubygems.org"
gem "hanami", "~> 2.1"
gem "hanami-assets", "~> 2.1"
gem "hanami-controller", "~> 2.1"
gem "hanami-router", "~> 2.1"
gem "hanami-validations", "~> 2.1"
gem "hanami-view", "~> 2.1"
gem "dry-types", "~> 1.0", ">= 1.6.1"
gem "puma"
gem "rake"
gem "rom", "~> 5.3"
gem "rom-sql", "~> 3.6"
gem "sqlite3"
gem "bcrypt", "~> 3.1"
gem "good_job", "~> 4.13"
group :development do
gem "guard-puma"
end
group :development, :test do
gem "dotenv"
end
group :cli, :development do
gem "hanami-reloader", "~> 2.1"
end
group :cli, :development, :test do
gem "hanami-rspec", "~> 2.1"
end
group :test do
gem "capybara"
gem "rack-test"
gem "playwright-ruby-client", "~> 1.40"
end
-9
View File
@@ -1,9 +0,0 @@
# frozen_string_literal: true
group :server do
guard "puma", port: ENV.fetch("HANAMI_PORT", 2300) do
# Edit the following regular expression for your needs.
# See: https://guides.hanamirb.org/app/code-reloading/
watch(%r{^(app|config|lib|slices)([\/][^\/]+)*.(rb|erb|haml|slim)$}i)
end
end
-2
View File
@@ -1,2 +0,0 @@
web: bundle exec hanami server
assets: bundle exec hanami assets watch
+21 -537
View File
@@ -1,552 +1,36 @@
# EuchreCamp
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
A comprehensive tournament management and partnership analytics system for the card game Euchre.
## Getting Started
## Overview
EuchreCamp is a full-stack Ruby web application built with Hanami 2.1 that provides:
- **Tournament Management**: Create and manage round-robin, single elimination, double elimination, and Swiss-style tournaments
- **Partnership Analytics**: Track partnership performance, win rates, and Elo changes between players
- **Player Profiles**: Display individual player statistics and partnership breakdowns
- **Admin Dashboard**: Centralized management interface for tournaments, players, and matches
- **Authentication & Authorization**: Role-based access control with player, tournament_admin, and club_admin roles
## Quick Start
### Prerequisites
- Ruby 3.3.3 (managed by mise or rbenv)
- Node.js 22+ (for asset compilation)
- SQLite3
- Bundler
### Installation
1. **Clone the repository:**
First, run the development server:
```bash
git clone <repository-url>
cd euchre_camp
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
2. **Install dependencies:**
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
```bash
bundle install
npm install
```
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
3. **Set up the database:**
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
```bash
bundle exec rake db:migrate
```
## Learn More
4. **Compile assets:**
To learn more about Next.js, take a look at the following resources:
```bash
./node_modules/.bin/esbuild app/assets/css/app.css --bundle --outfile=public/assets/app-GVDAEYEC.css
```
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
5. **Start the server:**
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
```bash
bundle exec puma -p 3000 -e development
```
## Deploy on Vercel
### Accessing the Application
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
- **Main Page**: http://localhost:3000
- **Admin Dashboard**: http://localhost:3000/admin
- **Rankings**: http://localhost:3000/rankings
- **Login**: http://localhost:3000/login
## Development
### Project Structure
```
euchre_camp/
├── app/
│ ├── actions/ # Hanami actions (controllers)
│ ├── assets/ # CSS, JavaScript, images
│ ├── repositories/ # ROM repositories for data access
│ ├── services/ # Business logic (Elo calculator, partnership tracker)
│ ├── templates/ # ERB view templates
│ ├── views/ # Hanami view objects
│ └── action.rb # Base action class with auth helpers
├── config/
│ ├── routes.rb # Application routes
│ ├── app.rb # Hanami app configuration
│ └── assets.js # Asset compilation configuration
├── db/
│ ├── migrate/ # Database migrations
│ └── euchre_camp.db # SQLite database
├── lib/
│ └── euchre_camp/ # Domain models and entities
├── spec/
│ └── acceptance/ # RSpec acceptance tests
└── public/
└── assets/ # Compiled assets
```
### Running the Application
**Development mode (with auto-reload):**
```bash
bundle exec puma -p 3000 -e development
```
**Production mode:**
```bash
bundle exec puma -p 3000 -e production
```
### Database Management
**Run migrations:**
```bash
bundle exec rake db:migrate
```
**Rollback last migration:**
```bash
bundle exec rake db:migrate[<version>]
```
**Reset database:**
```bash
bundle exec rake db:reset
```
### Testing
**Run all acceptance tests:**
```bash
bundle exec rspec spec/acceptance/
```
**Run specific test:**
```bash
bundle exec rspec spec/acceptance/tournament_partnership_spec.rb:280
```
**Run with documentation:**
```bash
bundle exec rspec spec/acceptance/ --format documentation
```
**Run Playwright mobile responsiveness tests:**
```bash
# Start the server
bundle exec hanami server --port 2300 &
# Run Playwright tests
bundle exec rspec spec/playwright/mobile_responsiveness_spec.rb
```
**Test with Playwright MCP (browser automation):**
1. Start the Hanami server: `bundle exec hanami server --port 2300`
2. Use opencode with Playwright MCP tools to:
- Navigate to pages
- Take screenshots
- Test responsive layouts
- Verify mobile UI elements
### Assets
**Compile CSS for development:**
```bash
./node_modules/.bin/esbuild app/assets/css/app.css --bundle --outfile=public/assets/app-GVDAEYEC.css
```
**Watch for changes (requires separate process):**
```bash
./node_modules/.bin/esbuild app/assets/css/app.css --bundle --outfile=public/assets/app-GVDAEYEC.css --watch
```
## Features
### Tournament Management
#### Creating a Tournament
1. Navigate to Admin Dashboard (`/admin`)
2. Click "Create Tournament"
3. Enter tournament details:
- Name
- Format (round-robin, single elimination, double elimination, Swiss)
- Number of teams/players
4. Register participants
5. Generate schedule
6. Record match results
#### Tournament Formats
- **Round-Robin**: Each team plays every other team once
- **Single Elimination**: Lose once and you're out
- **Double Elimination**: Must lose twice to be eliminated
- **Swiss**: Pairings based on win-loss records
### Partnership Analytics
#### Tracking Partnerships
The system automatically tracks:
- Games played together
- Win/loss records
- Elo changes per partnership
- Partnership win rates
#### Viewing Partnership Data
1. Visit any player's profile page (`/players/:id/profile`)
2. Scroll to "Partnership Performance" section
3. View statistics for each partner
### Player Profiles
Each player profile displays:
- Current Elo rating
- Total games played
- Win rate
- Partnership statistics
- Recent games timeline
### Admin Dashboard
The admin dashboard provides:
- Quick statistics (total players, active tournaments, recent matches)
- Quick action buttons for common tasks
- Recent matches table
- Links to all admin sections
## Authentication & Authorization
### User Roles
| Role | Permissions |
|------|-------------|
| **Player** | Edit own profile, record own matches within 5 minutes |
| **Tournament Admin** | Create/manage tournaments, edit matches in their tournaments (no time limit) |
| **Club Admin** | Full access to edit/delete any record, manage all players and tournaments |
### Login/Logout
**Login:**
- Navigate to `/login`
- Enter email and password
- Click "Login"
**Logout:**
- Click "Logout" in navigation bar
### Admin User Creation
To create an admin user, use the provided script:
```bash
ruby scripts/create_admin.rb <email> <password>
```
**Example:**
```bash
ruby scripts/create_admin.rb david@dhg.lol admin
```
This will:
1. Create a player profile
2. Create a user account with `club_admin` role
3. Mark the account as confirmed
**Manual SQL Alternative:**
If you prefer to create users manually via SQL:
1. Access the database: `sqlite3 db/euchre_camp.db`
2. Generate a BCrypt password hash:
```bash
ruby -e "require 'bcrypt'; puts BCrypt::Password.create('your_password')"
```
3. Insert a user record:
```sql
INSERT INTO players (name, rating) VALUES ('PlayerName', 1000);
INSERT INTO users (player_id, email, password_digest, role, confirmed, created_at, updated_at)
VALUES (1, 'user@example.com', '$2b$12$...', 'club_admin', 1, datetime('now'), datetime('now'));
```
## API Reference
### Routes
#### Public Routes
- `GET /` - Redirects to rankings
- `GET /rankings` - Player rankings
- `GET /players/:id` - Player profile
- `GET /players/:id/profile` - Player profile with partnerships
- `GET /players/:id/schedule` - Player tournament schedule
#### Authentication Routes
- `GET /login` - Login form
- `POST /auth/login` - Process login
- `GET /logout` - Logout and clear session
#### Admin Routes (Require Authentication)
- `GET /admin` - Admin dashboard
- `GET /admin/players` - List all players
- `GET /admin/players/new` - Add new player form
- `POST /admin/players` - Create new player
- `GET /admin/players/:id/edit` - Edit player form
- `PATCH /admin/players/:id` - Update player
- `DELETE /admin/players/:id` - Delete player
- `GET /admin/matches` - List all matches
- `GET /admin/matches/new` - Record match form
- `POST /admin/matches` - Create match
- `GET /admin/matches/:id/edit` - Edit match form
- `PATCH /admin/matches/:id` - Update match
- `GET /admin/matches/upload` - CSV upload form
- `POST /admin/matches/process_upload` - Process CSV upload
- `GET /admin/tournaments` - List all tournaments
- `GET /admin/tournaments/new` - Create tournament form
- `POST /admin/tournaments` - Create tournament
- `GET /admin/tournaments/:id` - Tournament details
- `GET /admin/tournaments/:id/edit` - Edit tournament form
- `PATCH /admin/tournaments/:id` - Update tournament
- `DELETE /admin/tournaments/:id` - Delete tournament
- `POST /admin/tournaments/:id/participants` - Add participant
- `DELETE /admin/tournaments/:id/participants/:player_id` - Remove participant
- `POST /admin/tournaments/:id/teams` - Create team
- `GET /admin/tournaments/:id/teams/:team_id/edit` - Edit team form
- `PATCH /admin/tournaments/:id/teams/:team_id` - Update team
- `DELETE /admin/tournaments/:id/teams/:team_id` - Delete team
- `POST /admin/tournaments/:id/schedule` - Generate schedule
- `GET /admin/tournaments/:id/rounds/:round_id` - View round matchups
- `GET /admin/tournaments/:id/results` - Tournament results
- `POST /admin/tournaments/:id/results` - Save results
- `POST /admin/tournaments/:id/complete` - Complete tournament
## Database Schema
### Tables
- **players**: Player information and ratings
- **users**: Authentication and authorization
- **events**: Tournaments and events
- **event_participants**: Player participation in tournaments
- **teams**: Teams in tournaments
- **tournament_rounds**: Tournament rounds
- **bracket_matchups**: Matchups per round
- **matches**: Individual match results
- **elo_snapshots**: Elo rating history
- **partnership_games**: Partnership occurrence tracking
- **partnership_stats**: Aggregated partnership statistics
## Common Tasks
### Adding a New Player
1. Navigate to Admin Dashboard
2. Click "Add Player"
3. Enter name and initial rating (default 1000)
4. Click "Create Player"
### Recording a Match
1. Navigate to Admin Dashboard
2. Click "Record Match Result"
3. Select players for Team 1 and Team 2
4. Enter scores
5. Click "Create Match"
### Creating a Tournament
1. Navigate to Admin Dashboard
2. Click "Create Tournament"
3. Enter tournament details
4. Register participants
5. Generate schedule
6. Start recording results
### Editing a Match
**Within 5 minutes (any user):**
1. Navigate to `/admin/matches`
2. Click "Edit" next to the match
3. Update scores
4. Click "Update Match"
**Anytime (tournament admin or club admin):**
- Same process as above, no time restriction
## Troubleshooting
### Common Issues
**Server won't start:**
- Check if port 3000 is already in use: `lsof -i :3000`
- Kill existing process: `kill -9 <PID>`
- Try a different port: `bundle exec puma -p 3001`
**CSS not loading:**
- Recompile assets: `./node_modules/.bin/esbuild app/assets/css/app.css --bundle --outfile=public/assets/app-GVDAEYEC.css`
- Clear browser cache
**Database errors:**
- Run migrations: `bundle exec rake db:migrate`
- Reset database: `bundle exec rake db:reset`
**Authorization errors:**
- Ensure you're logged in
- Check user role in database
### Debugging
**Check server logs:**
```bash
tail -f /tmp/puma.log
```
**Check database:**
```bash
sqlite3 db/euchre_camp.db
sqlite> .tables
sqlite> SELECT * FROM users;
```
## Development Workflow
### Making Changes
1. Create a feature branch: `git checkout -b feature/your-feature`
2. Make changes to code
3. Run tests: `bundle exec rspec spec/acceptance/`
4. Commit with conventional commit message
5. Push to remote: `git push origin feature/your-feature`
6. Create pull request
### Commit Message Format
```
<type>: <description>
[optional body]
[optional footer]
```
Types:
- `feat`: New feature
- `fix`: Bug fix
- `docs`: Documentation changes
- `style`: Code style changes
- `refactor`: Code refactoring
- `test`: Test changes
- `chore`: Build/dependency changes
### Code Style
- Follow existing code patterns in the project
- Use ROM (Ruby Object Mapper) for database access
- Keep business logic in services
- Use dependency injection in actions
- Write acceptance tests for new features
## Deployment
### Production Environment
1. **Set production environment variables:**
```bash
export HANAMI_ENV=production
export DATABASE_URL=sqlite:///path/to/prod.db
export SESSION_SECRET=<secure-random-string>
```
2. **Compile assets for production:**
```bash
./node_modules/.bin/esbuild app/assets/css/app.css --bundle --outfile=public/assets/app-GVDAEYEC.css
```
3. **Run migrations:**
```bash
bundle exec rake db:migrate
```
4. **Start server:**
```bash
bundle exec puma -p 3000 -e production
```
### Using a Process Manager
**Using systemd:**
```ini
[Unit]
Description=EuchreCamp
After=network.target
[Service]
Type=simple
User=deploy
WorkingDirectory=/var/www/euchre_camp
Environment=HANAMI_ENV=production
ExecStart=/usr/local/bin/bundle exec puma -C config/puma.rb
Restart=always
[Install]
WantedBy=multi-user.target
```
**Using PM2 (Node.js):**
```bash
pm2 start bundle exec -- puma -C config/puma.rb
```
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Run tests to ensure they pass
5. Submit a pull request
## License
MIT License - see LICENSE file for details
## Credits
Built with:
- Hanami 2.1
- ROM (Ruby Object Mapper)
- SQLite3
- RSpec
- Puma
## Additional Resources
- [Hanami Documentation](https://guides.hanamirb.org/)
- [ROM Documentation](https://rom-rb.org/)
- [RSpec Documentation](https://rspec.info/)
- [Euchre Rules](https://www.pagat.com/euchre/euchre.html)
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
-16
View File
@@ -1,16 +0,0 @@
# frozen_string_literal: true
require "hanami/rake_tasks"
require 'rom/sql/rake_task'
task :environment do
require_relative 'config/app'
require 'hanami/prepare'
end
namespace :db do
task setup: :environment do
Hanami.app.prepare(:persistence)
ROM::SQL::RakeSupport.env = Hanami.app['persistence.config']
end
end
-97
View File
@@ -1,97 +0,0 @@
# auto_register: false
# frozen_string_literal: true
require "hanami/action"
module EuchreCamp
class Action < Hanami::Action
include Deps[
users_repo: 'repositories.users',
players_repo: 'repositories.players',
matches_repo: 'repositories.matches'
]
protected
# Get current user from session
def current_user(request)
user_id = request.session[:user_id]
return nil unless user_id
users_repo.by_id(user_id)
end
# Get current player from session
def current_player(request)
player_id = request.session[:player_id]
return nil unless player_id
players_repo.by_id(player_id)
end
# Check if user is logged in
def logged_in?(request)
!request.session[:user_id].nil?
end
# Require authentication - redirect to login if not logged in
def require_authentication(request, response)
unless logged_in?(request)
response.flash[:error] = 'Please login to continue'
response.redirect_to '/login'
end
end
# Require specific role
def require_role(request, response, *allowed_roles)
user = current_user(request)
return false unless user
unless allowed_roles.include?(user.role.to_sym)
response.flash[:error] = 'Access denied'
response.redirect_to '/rankings'
return false
end
true
end
# Check authorization for an action
def authorize(request, response, action, resource = nil, context = {})
user = current_user(request)
return false unless user
unless EuchreCamp::Services::Authorization.can?(user, action, resource, context)
response.flash[:error] = 'You do not have permission to perform this action'
response.redirect_to '/rankings'
return false
end
true
end
# Helper to pass current_player to view
def render_with_player(request, response, **options)
player = current_player(request)
response.render(view, current_player: player, **options)
end
# Get match and check if user can edit it
def get_editable_match(request, response, match_id)
match = matches_repo.by_id(match_id)
return nil unless match
user = current_user(request)
return match unless user
# Check if match is within 5 minutes for regular users
if EuchreCamp::Services::Authorization.can?(user, :edit_own_match_within_5_min, match)
match
else
response.flash[:error] = 'Match cannot be edited (too old or not your match)'
response.redirect_to '/admin/matches'
nil
end
end
end
end
View File
-73
View File
@@ -1,73 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
class Index < EuchreCamp::Action
include Deps[
players_repo: 'repositories.players',
events_repo: 'repositories.events',
matches_repo: 'repositories.matches'
]
def handle(request, response)
# Check if user has permission to view admin panel
user = current_user(request)
unless user && EuchreCamp::Services::Authorization.can?(user, :view_admin_panel)
response.flash[:error] = 'Access denied'
response.redirect_to '/rankings'
return
end
# Get dashboard statistics
total_players = players_repo.all.length
active_tournaments = events_repo.upcoming.length
completed_tournaments = events_repo.completed.length
recent_matches = get_recent_matches
user = current_user(request)
render_with_player(request, response,
total_players: total_players,
active_tournaments: active_tournaments,
completed_tournaments: completed_tournaments,
recent_matches: recent_matches,
current_user: user
)
end
private
def get_recent_matches
rom = EuchreCamp::App["persistence.rom"]
matches = rom.relations[:matches]
.order(Sequel.desc(:id))
.limit(5)
.to_a
# Get player names
all_players = rom.relations[:players].to_a.to_h { |p| [p[:id], p] }
matches.map do |match|
team_1 = [
all_players[match[:team_1_p1_id]],
all_players[match[:team_1_p2_id]]
].compact
team_2 = [
all_players[match[:team_2_p1_id]],
all_players[match[:team_2_p2_id]]
].compact
{
id: match[:id],
team_1: team_1,
team_2: team_2,
score: "#{match[:team_1_score]}-#{match[:team_2_score]}",
status: match[:status],
played_at: match[:played_at]
}
end
end
end
end
end
end
-78
View File
@@ -1,78 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class Create < EuchreCamp::Action
include Deps[
matches_repo: 'repositories.matches',
teams_repo: 'repositories.teams',
brackets_repo: 'repositories.bracket_matchups'
]
def handle(request, response)
params = request.params.to_h
tournament_id = params[:event_id]&.to_i
# Build match parameters
match_params = {
played_at: params[:played_at] ? Time.parse(params[:played_at]) : Time.now
}
if tournament_id && params[:team_1_id] && params[:team_2_id]
# Tournament match - use team IDs
team_1 = teams_repo.teams.by_pk(params[:team_1_id].to_i).one
team_2 = teams_repo.teams.by_pk(params[:team_2_id].to_i).one
match_params.merge!(
event_id: tournament_id,
team_1_p1_id: team_1.player_1_id,
team_1_p2_id: team_1.player_2_id,
team_2_p1_id: team_2.player_1_id,
team_2_p2_id: team_2.player_2_id,
team_1_score: params[:team_1_score]&.to_i || 0,
team_2_score: params[:team_2_score]&.to_i || 0,
status: 'completed'
)
else
# Non-tournament match - use individual players
match_params.merge!(
team_1_p1_id: params[:team_1_p1_id]&.to_i,
team_1_p2_id: params[:team_1_p2_id]&.to_i,
team_1_score: params[:team_1_score]&.to_i || 0,
team_2_p1_id: params[:team_2_p1_id]&.to_i,
team_2_p2_id: params[:team_2_p2_id]&.to_i,
team_2_score: params[:team_2_score]&.to_i || 0,
status: 'completed'
)
end
# Create the match
match = matches_repo.create(match_params)
# Enqueue background job to calculate Elo
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
# Redirect based on context
if tournament_id
response.redirect_to("/admin/tournaments/#{tournament_id}")
else
response.redirect_to("/admin/matches")
end
rescue StandardError => e
# Handle validation errors or other issues
puts "Error: #{e.message}"
puts e.backtrace.join("\n")
if tournament_id
response.redirect_to("/admin/tournaments/#{tournament_id}")
else
response.redirect_to("/admin/matches/new")
end
end
end
end
end
end
end
-42
View File
@@ -1,42 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class Edit < EuchreCamp::Action
include Deps[matches_repo: 'repositories.matches', players_repo: 'repositories.players']
def handle(request, response)
match_id = request.params[:id]
match = matches_repo.by_id(match_id)
user = current_user(request)
# Check authorization
if user
# Check if user can edit this match (within 5 minutes or admin)
unless EuchreCamp::Services::Authorization.can?(user, :edit_own_match_within_5_min, match) ||
EuchreCamp::Services::Authorization.can?(user, :edit_any_record)
response.flash[:error] = 'Match cannot be edited (too old or insufficient permissions)'
response.redirect_to '/admin/matches'
return
end
else
response.flash[:error] = 'Please login to edit matches'
response.redirect_to '/login'
return
end
# Get available players for the form
all_players = players_repo.all
render_with_player(request, response,
match: match,
all_players: all_players
)
end
end
end
end
end
end
-43
View File
@@ -1,43 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class Index < EuchreCamp::Action
include Deps[repo: 'repositories.matches']
def handle(request, response)
# Check authorization - view all matches
user = current_user(request)
unless user && EuchreCamp::Services::Authorization.can?(user, :view_all)
response.flash[:error] = 'Access denied'
response.redirect_to '/rankings'
return
end
matches = repo.all
enhanced_matches = enhance_matches_with_player_names(matches)
response.render(view, matches: enhanced_matches)
end
private
def enhance_matches_with_player_names(matches)
rom = EuchreCamp::App["persistence.rom"]
all_players = rom.relations[:players].to_a.to_h { |p| [p[:id], p] }
matches.map do |match|
match.to_h.merge(
team_1_p1_name: all_players[match.team_1_p1_id]&.[](:name) || 'Unknown',
team_1_p2_name: all_players[match.team_1_p2_id]&.[](:name) || 'Unknown',
team_2_p1_name: all_players[match.team_2_p1_id]&.[](:name) || 'Unknown',
team_2_p2_name: all_players[match.team_2_p2_id]&.[](:name) || 'Unknown'
)
end
end
end
end
end
end
end
-37
View File
@@ -1,37 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class New < EuchreCamp::Action
include Deps[
players_repo: 'repositories.players',
events_repo: 'repositories.events',
teams_repo: 'repositories.teams'
]
def handle(request, response)
players = players_repo.all
tournament = nil
teams = []
# Check if creating match for specific tournament
if request.params[:event_id]
tournament = events_repo.by_id(request.params[:event_id])
teams = teams_repo.by_event(tournament[:id])
end
response.render(view,
players: players,
tournament: tournament,
teams: teams,
preselected_team_1: request.params[:team_1_id]&.to_i,
preselected_team_2: request.params[:team_2_id]&.to_i
)
end
end
end
end
end
end
@@ -1,50 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class ProcessUpload < EuchreCamp::Action
include Deps[events_repo: 'repositories.events']
def handle(request, response)
# In Hanami, file uploads are in request.params
file = request.params[:csv_file]
event_id = request.params[:event_id]&.to_i
unless file && file[:tempfile]
response.flash[:error] = "No file uploaded"
response.redirect_to("/admin/matches/upload")
return
end
unless event_id
response.flash[:error] = "Please select a tournament"
response.redirect_to("/admin/matches/upload")
return
end
# Verify event exists
begin
events_repo.by_id(event_id)
rescue
response.flash[:error] = "Invalid tournament ID"
response.redirect_to("/admin/matches/upload")
return
end
# Use the specialized importer for Euchre tournament results
importer = EuchreCamp::Services::TournamentCsvImporter.new(event_id: event_id)
results = importer.import(file[:tempfile].path)
message = "Created #{results[:matches_created]} matches, #{results[:teams_created]} teams"
message += ". Errors: #{results[:errors].join(', ')}" if results[:errors].any?
response.flash[:message] = message
response.redirect_to("/admin/tournaments/#{event_id}")
end
end
end
end
end
end
-55
View File
@@ -1,55 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class Update < EuchreCamp::Action
include Deps[matches_repo: 'repositories.matches']
def handle(request, response)
match_id = request.params[:id]
match = matches_repo.by_id(match_id)
user = current_user(request)
# Check authorization
if user
# Check if user can edit this match (within 5 minutes or admin)
unless EuchreCamp::Services::Authorization.can?(user, :edit_own_match_within_5_min, match) ||
EuchreCamp::Services::Authorization.can?(user, :edit_any_record)
response.flash[:error] = 'Match cannot be edited (too old or insufficient permissions)'
response.redirect_to '/admin/matches'
return
end
else
response.flash[:error] = 'Please login to edit matches'
response.redirect_to '/login'
return
end
# Update the match
match_data = request.params[:match]
begin
rom = EuchreCamp::App["persistence.rom"]
rom.relations[:matches].where(id: match_id).update(
team_1_p1_id: match_data[:team_1_p1_id],
team_1_p2_id: match_data[:team_1_p2_id],
team_2_p1_id: match_data[:team_2_p1_id],
team_2_p2_id: match_data[:team_2_p2_id],
team_1_score: match_data[:team_1_score],
team_2_score: match_data[:team_2_score],
status: 'completed'
)
response.flash[:success] = 'Match updated successfully'
response.redirect_to '/admin/matches'
rescue => e
response.flash[:error] = "Failed to update match: #{e.message}"
response.redirect_to "/admin/matches/#{match_id}/edit"
end
end
end
end
end
end
end
-18
View File
@@ -1,18 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Matches
class Upload < EuchreCamp::Action
include Deps[events_repo: 'repositories.events']
def handle(request, response)
tournaments = events_repo.all
response.render(view, tournaments: tournaments)
end
end
end
end
end
end
-14
View File
@@ -1,14 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Players
class Create < EuchreCamp::Action
def handle(request, response)
end
end
end
end
end
end
-14
View File
@@ -1,14 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Players
class Destroy < EuchreCamp::Action
def handle(request, response)
end
end
end
end
end
end
-27
View File
@@ -1,27 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Players
class Edit < EuchreCamp::Action
include Deps[players_repo: 'repositories.players']
def handle(request, response)
player_id = request.params[:id]
player = players_repo.by_id(player_id)
user = current_user(request)
# Authorization: User can edit own profile, or admin can edit any
unless authorize(request, response, :edit_own_profile, { id: player_id }) ||
authorize(request, response, :edit_any_record)
return
end
render_with_player(request, response, player: player)
end
end
end
end
end
end
-18
View File
@@ -1,18 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Players
class Index < EuchreCamp::Action
include Deps[repo: 'repositories.players']
def handle(_request, response)
players = repo.all
response.render(view, players: players)
end
end
end
end
end
end
-14
View File
@@ -1,14 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Players
class New < EuchreCamp::Action
def handle(request, response)
end
end
end
end
end
end
-14
View File
@@ -1,14 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Players
class Update < EuchreCamp::Action
def handle(request, response)
end
end
end
end
end
end
-25
View File
@@ -1,25 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Complete < EuchreCamp::Action
include Deps[events_repo: 'repositories.events']
def handle(request, response)
tournament_id = request.params[:id]
events_repo.update(tournament_id, status: 'completed')
response.flash[:message] = "Tournament completed"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to complete tournament: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}")
end
end
end
end
end
end
-34
View File
@@ -1,34 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Create < EuchreCamp::Action
include Deps[repo: 'repositories.events']
def handle(request, response)
params = request.params.to_h
event_params = {
name: params[:name],
description: params[:description],
event_date: params[:event_date] ? Time.parse(params[:event_date]) : nil,
event_type: 'tournament',
format: params[:format] || 'single_elim',
status: 'planned',
max_participants: params[:max_participants]&.to_i
}
event = repo.create(event_params)
response.redirect_to("/admin/tournaments/#{event[:id]}")
rescue StandardError => e
response.flash[:error] = "Failed to create tournament: #{e.message}"
response.redirect_to("/admin/tournaments/new")
end
end
end
end
end
end
-23
View File
@@ -1,23 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Destroy < EuchreCamp::Action
include Deps[repo: 'repositories.events']
def handle(request, response)
tournament_id = request.params[:id]
repo.delete(tournament_id)
response.flash[:message] = "Tournament deleted"
response.redirect_to("/admin/tournaments")
rescue StandardError => e
response.flash[:error] = "Failed to delete: #{e.message}"
response.redirect_to("/admin/tournaments")
end
end
end
end
end
end
-19
View File
@@ -1,19 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Edit < EuchreCamp::Action
include Deps[repo: 'repositories.events']
def handle(request, response)
tournament_id = request.params[:id]
tournament = repo.by_id(tournament_id)
response.render(view, tournament: tournament)
end
end
end
end
end
end
-19
View File
@@ -1,19 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Index < EuchreCamp::Action
include Deps[repo: 'repositories.events']
def handle(_request, response)
upcoming = repo.upcoming
completed = repo.completed
response.render(view, upcoming: upcoming, completed: completed)
end
end
end
end
end
end
-15
View File
@@ -1,15 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class New < EuchreCamp::Action
def handle(_request, response)
response.render(view)
end
end
end
end
end
end
@@ -1,34 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
module Participants
class Create < EuchreCamp::Action
include Deps[participants_repo: 'repositories.event_participants']
def handle(request, response)
tournament_id = request.params[:id]
player_id = request.params[:player_id]
if player_id.nil? || player_id.empty?
response.flash[:error] = "Please select a player"
response.redirect_to("/admin/tournaments/#{tournament_id}")
return
end
participants_repo.register_player(tournament_id, player_id.to_i)
response.flash[:message] = "Player added to tournament"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to add player: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}")
end
end
end
end
end
end
end
@@ -1,28 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
module Participants
class Destroy < EuchreCamp::Action
include Deps[participants_repo: 'repositories.event_participants']
def handle(request, response)
tournament_id = request.params[:id]
player_id = request.params[:player_id]
participants_repo.remove_player(tournament_id, player_id.to_i)
response.flash[:message] = "Player removed from tournament"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to remove player: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}")
end
end
end
end
end
end
end
-41
View File
@@ -1,41 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Results < EuchreCamp::Action
include Deps[
events_repo: 'repositories.events',
rounds_repo: 'repositories.tournament_rounds',
matchups_repo: 'repositories.bracket_matchups',
teams_repo: 'repositories.teams'
]
def handle(request, response)
tournament_id = request.params[:id]
tournament = events_repo.by_id(tournament_id)
current_round = rounds_repo.current_round(tournament_id)
if current_round.nil?
response.flash[:error] = "No active round to enter results"
response.redirect_to("/admin/tournaments/#{tournament_id}")
return
end
matchups = matchups_repo.by_round(current_round[:id])
teams = teams_repo.by_event(tournament_id)
response.render(view,
tournament: tournament,
round: current_round,
matchups: matchups,
teams: teams
)
end
end
end
end
end
end
@@ -1,37 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
module Rounds
class Show < EuchreCamp::Action
include Deps[
events_repo: 'repositories.events',
rounds_repo: 'repositories.tournament_rounds',
matchups_repo: 'repositories.bracket_matchups',
teams_repo: 'repositories.teams'
]
def handle(request, response)
tournament_id = request.params[:id]
round_id = request.params[:round_id]
tournament = events_repo.by_id(tournament_id)
round = rounds_repo.by_event(tournament_id).find { |r| r[:id] == round_id.to_i }
matchups = matchups_repo.by_round(round_id)
teams = teams_repo.by_event(tournament_id)
response.render(view,
tournament: tournament,
round: round,
matchups: matchups,
teams: teams
)
end
end
end
end
end
end
end
@@ -1,75 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class SaveResults < EuchreCamp::Action
include Deps[
events_repo: 'repositories.events',
rounds_repo: 'repositories.tournament_rounds',
matchups_repo: 'repositories.bracket_matchups',
teams_repo: 'repositories.teams',
matches_repo: 'repositories.matches'
]
def handle(request, response)
tournament_id = request.params[:id]
params = request.params.to_h
# Process each matchup result
params.each do |key, value|
key_str = key.to_s
if key_str.start_with?('matchup_')
matchup_id = key_str.gsub('matchup_', '').to_i
team_1_score = params["team_1_score_#{matchup_id}"]&.to_i || 0
team_2_score = params["team_2_score_#{matchup_id}"]&.to_i || 0
matchup = matchups_repo.bracket_matchups.by_pk(matchup_id).one
unless matchup && matchup[:team_1_id] && matchup[:team_2_id]
next
end
# Create match record
team_1 = teams_repo.teams.by_pk(matchup[:team_1_id]).one
team_2 = teams_repo.teams.by_pk(matchup[:team_2_id]).one
next if team_1.nil? || team_2.nil?
match = matches_repo.create(
event_id: tournament_id,
team_1_p1_id: team_1.player_1_id,
team_1_p2_id: team_1.player_2_id,
team_2_p1_id: team_2.player_1_id,
team_2_p2_id: team_2.player_2_id,
team_1_score: team_1_score,
team_2_score: team_2_score,
status: 'completed'
)
# Update matchup with match and winner
winner_id = team_1_score > team_2_score ? matchup[:team_1_id] : matchup[:team_2_id]
loser_id = team_1_score > team_2_score ? matchup[:team_2_id] : matchup[:team_1_id]
matchups_repo.update(matchup_id,
match_id: match[:id],
winner_team_id: winner_id,
loser_team_id: loser_id
)
# Enqueue Elo calculation
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
end
end
response.flash[:message] = "Results saved"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to save results: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}/results")
end
end
end
end
end
end
-126
View File
@@ -1,126 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Schedule < EuchreCamp::Action
include Deps[
events_repo: 'repositories.events',
teams_repo: 'repositories.teams',
rounds_repo: 'repositories.tournament_rounds',
matchups_repo: 'repositories.bracket_matchups'
]
def handle(request, response)
tournament_id = request.params[:id]
tournament = events_repo.by_id(tournament_id)
teams = teams_repo.by_event(tournament_id)
if teams.empty?
response.flash[:error] = "No teams to schedule. Add teams first."
response.redirect_to("/admin/tournaments/#{tournament_id}")
return
end
# Generate matchups based on format
teams_data = teams.map { |t| { id: t[:id], seed: t[:id] } }
format = tournament[:format]
begin
case format
when 'round_robin'
rounds = EuchreCamp::Services::TournamentGenerator.round_robin(teams_data)
create_round_robin_schedule(tournament_id, rounds)
when 'single_elim'
bracket = EuchreCamp::Services::TournamentGenerator.single_elimination(teams_data)
create_single_elim_schedule(tournament_id, bracket, teams)
when 'swiss'
# For Swiss, create first round
rounds = EuchreCamp::Services::TournamentGenerator.swiss(teams_data, 1)
create_swiss_schedule(tournament_id, rounds, 1)
else
response.flash[:error] = "Format '#{format}' not yet implemented"
response.redirect_to("/admin/tournaments/#{tournament_id}")
return
end
# Update tournament status
events_repo.update(tournament_id, status: 'active')
response.flash[:message] = "Schedule generated successfully"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to generate schedule: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}")
end
end
private
def create_round_robin_schedule(tournament_id, rounds)
rounds.each do |round_data|
round = rounds_repo.create(
event_id: tournament_id,
round_number: round_data[:round_number],
status: round_data[:round_number] == 1 ? 'active' : 'pending'
)
round_data[:matchups].each_with_index do |matchup, index|
matchups_repo.create(
event_id: tournament_id,
round_id: round[:id],
team_1_id: matchup[:team_1_id],
team_2_id: matchup[:team_2_id],
bracket_position: index + 1
)
end
end
end
def create_single_elim_schedule(tournament_id, bracket, teams)
# Create first round
round = rounds_repo.create(
event_id: tournament_id,
round_number: 1,
status: 'active'
)
bracket[:first_round_matchups].each do |matchup|
# Skip if team is nil (BYE)
next if matchup[:team_1_id].nil? && matchup[:team_2_id].nil?
matchups_repo.create(
event_id: tournament_id,
round_id: round[:id],
team_1_id: matchup[:team_1_id],
team_2_id: matchup[:team_2_id],
bracket_position: matchup[:bracket_position]
)
end
end
def create_swiss_schedule(tournament_id, rounds, round_number)
round = rounds_repo.create(
event_id: tournament_id,
round_number: round_number,
status: 'active'
)
rounds.each do |round_data|
round_data[:matchups].each_with_index do |matchup, index|
matchups_repo.create(
event_id: tournament_id,
round_id: round[:id],
team_1_id: matchup[:team_1_id],
team_2_id: matchup[:team_2_id],
bracket_position: index + 1
)
end
end
end
end
end
end
end
end
-62
View File
@@ -1,62 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Show < EuchreCamp::Action
include Deps[
events_repo: 'repositories.events',
participants_repo: 'repositories.event_participants',
teams_repo: 'repositories.teams',
rounds_repo: 'repositories.tournament_rounds',
players_repo: 'repositories.players'
]
def handle(request, response)
tournament_id = request.params[:id]
tournament = events_repo.by_id(tournament_id)
user = current_user(request)
# Check authorization - tournament admin can manage their tournaments
unless user && EuchreCamp::Services::Authorization.can?(user, :manage_tournament, tournament)
response.flash[:error] = 'Access denied'
response.redirect_to '/rankings'
return
end
# Get all registered participants for this tournament
participants = participants_repo.by_event(tournament_id)
participant_ids = participants.map { |p| p[:player_id] }
# Get registered players details
players = if participant_ids.any?
players_repo.players.where(id: participant_ids).to_a
else
[]
end
# Get ALL players for the registration dropdown (not yet registered)
all_players = players_repo.all
available_players = all_players.reject { |p| participant_ids.include?(p[:id]) }
# Get teams
teams = teams_repo.by_event(tournament_id)
# Get rounds
rounds = rounds_repo.by_event(tournament_id)
response.render(view,
tournament: tournament,
players: players,
available_players: available_players,
teams: teams,
rounds: rounds,
participants: participants
)
end
end
end
end
end
end
@@ -1,62 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
module Teams
class Create < EuchreCamp::Action
include Deps[
teams_repo: 'repositories.teams',
players_repo: 'repositories.players',
participants_repo: 'repositories.event_participants'
]
def handle(request, response)
tournament_id = request.params[:id]
params = request.params.to_h
player_1_id = params[:player_1_id]&.to_i
player_2_id = params[:player_2_id]&.to_i
team_name = params[:team_name]
if player_1_id.nil? || player_2_id.nil?
response.flash[:error] = "Please select both players"
response.redirect_to("/admin/tournaments/#{tournament_id}")
return
end
if player_1_id == player_2_id
response.flash[:error] = "Players must be different"
response.redirect_to("/admin/tournaments/#{tournament_id}")
return
end
# Ensure players are registered as participants
[player_1_id, player_2_id].each do |player_id|
existing = participants_repo.by_event(tournament_id).find { |p| p[:player_id] == player_id }
unless existing
participants_repo.register_player(tournament_id, player_id)
end
end
# Create the team
team = teams_repo.create(
event_id: tournament_id,
player_1_id: player_1_id,
player_2_id: player_2_id,
team_name: team_name || "Team #{player_1_id}-#{player_2_id}"
)
response.flash[:message] = "Team created successfully"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to create team: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}")
end
end
end
end
end
end
end
@@ -1,28 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
module Teams
class Destroy < EuchreCamp::Action
include Deps[teams_repo: 'repositories.teams']
def handle(request, response)
tournament_id = request.params[:id]
team_id = request.params[:team_id]
teams_repo.delete(team_id.to_i)
response.flash[:message] = "Team removed"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to remove team: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}")
end
end
end
end
end
end
end
@@ -1,34 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
module Teams
class Edit < EuchreCamp::Action
include Deps[
teams_repo: 'repositories.teams',
players_repo: 'repositories.players',
events_repo: 'repositories.events'
]
def handle(request, response)
tournament_id = request.params[:id]
team_id = request.params[:team_id]
tournament = events_repo.by_id(tournament_id)
team = teams_repo.teams.by_pk(team_id).one
players = players_repo.all
response.render(view,
tournament: tournament,
team: team,
players: players
)
end
end
end
end
end
end
end
@@ -1,35 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
module Teams
class Update < EuchreCamp::Action
include Deps[teams_repo: 'repositories.teams']
def handle(request, response)
tournament_id = request.params[:id]
team_id = request.params[:team_id]
params = request.params.to_h
update_params = {
team_name: params[:team_name],
player_1_id: params[:player_1_id]&.to_i,
player_2_id: params[:player_2_id]&.to_i
}
teams_repo.update(team_id.to_i, update_params)
response.flash[:message] = "Team updated"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to update team: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}/teams/#{team_id}/edit")
end
end
end
end
end
end
end
-34
View File
@@ -1,34 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Admin
module Tournaments
class Update < EuchreCamp::Action
include Deps[repo: 'repositories.events']
def handle(request, response)
tournament_id = request.params[:id]
params = request.params.to_h
update_params = {
name: params[:name],
description: params[:description],
event_date: params[:event_date] ? Time.parse(params[:event_date]) : nil,
format: params[:format],
max_participants: params[:max_participants]&.to_i
}
repo.update(tournament_id, update_params)
response.flash[:message] = "Tournament updated"
response.redirect_to("/admin/tournaments/#{tournament_id}")
rescue StandardError => e
response.flash[:error] = "Failed to update: #{e.message}"
response.redirect_to("/admin/tournaments/#{tournament_id}/edit")
end
end
end
end
end
end
-63
View File
@@ -1,63 +0,0 @@
# frozen_string_literal: true
require 'bcrypt'
module EuchreCamp
module Actions
module Auth
module Login
class Create < EuchreCamp::Action
include Deps[users_repo: 'repositories.users', players_repo: 'repositories.players']
def handle(request, response)
email = request.params[:email]
password = request.params[:password]
user = users_repo.by_email(email)
if user && valid_password?(user, password)
# Check if account is locked
locked_until = user[:locked_until]
if locked_until && locked_until > Time.now
response.flash[:error] = 'Account is locked. Please contact support.'
response.redirect_to '/login'
return
end
# Check if account is confirmed
unless user[:confirmed]
response.flash[:error] = 'Account not confirmed. Please check your email.'
response.redirect_to '/login'
return
end
# Record successful login
ip_address = request.env['REMOTE_ADDR']
users_repo.record_login(user[:id], ip_address)
# Create session
session = request.session
session[:user_id] = user[:id]
session[:player_id] = user[:player_id]
response.flash[:success] = 'Welcome back!'
response.redirect_to '/'
else
# Record failed login
users_repo.record_failed_login(user[:id]) if user
response.flash[:error] = 'Invalid email or password'
response.redirect_to '/login'
end
end
private
def valid_password?(user, password)
BCrypt::Password.new(user[:password_digest]) == password
end
end
end
end
end
end
-27
View File
@@ -1,27 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Auth
module Login
class Show < EuchreCamp::Action
include Deps[users_repo: 'repositories.users', players_repo: 'repositories.players']
def handle(request, response)
player = current_player(request)
response.render(view, current_player: player)
end
protected
def current_player(request)
player_id = request.session[:player_id]
return nil unless player_id
players_repo.by_id(player_id)
end
end
end
end
end
end
-14
View File
@@ -1,14 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Auth
class Logout < EuchreCamp::Action
def handle(request, response)
request.session.clear
response.redirect_to '/rankings'
end
end
end
end
end
-75
View File
@@ -1,75 +0,0 @@
# 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],
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
-17
View File
@@ -1,17 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Players
class Rankings < EuchreCamp::Action
include Deps[repo: 'repositories.players']
def handle(request, response)
# Get all players and sort by current Elo
players = repo.all.sort_by { |p| -p.current_elo }
render_with_player(request, response, players: players)
end
end
end
end
end
-128
View File
@@ -1,128 +0,0 @@
# 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
-17
View File
@@ -1,17 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Actions
module Players
class Show < EuchreCamp::Action
include Deps[ repo: 'repositories.players' ]
def handle(request, response)
requested_player = request.params[:id]
player = repo.by_id(requested_player)
response.render(view, player: player)
end
end
end
end
end
-987
View File
@@ -1,987 +0,0 @@
/* EuchreCamp Theme - Green and Gold */
:root {
/* Primary Colors */
--color-primary: #2d5a27;
--color-primary-dark: #1e3d1a;
--color-primary-light: #4a7c42;
/* Secondary Colors (Gold) */
--color-secondary: #c9a227;
--color-secondary-dark: #a68a1f;
--color-secondary-light: #e6b832;
/* Neutral Colors */
--color-white: #ffffff;
--color-gray-50: #f9f9f9;
--color-gray-100: #f5f5f5;
--color-gray-200: #e0e0e0;
--color-gray-300: #d0d0d0;
--color-gray-500: #666666;
--color-gray-700: #333333;
--color-black: #000000;
/* Status Colors */
--color-success: #2d5a27;
--color-error: #c62828;
--color-warning: #c9a227;
--color-info: #1565c0;
/* Background Colors */
--bg-body: var(--color-white);
--bg-card: var(--color-white);
--bg-card-alt: var(--color-gray-50);
--bg-nav: var(--color-primary);
--bg-nav-mobile: var(--color-primary);
--bg-bottom-nav: var(--color-primary);
/* Text Colors */
--text-primary: var(--color-black);
--text-secondary: var(--color-gray-500);
--text-on-primary: var(--color-white);
--text-on-secondary: var(--color-black);
/* Spacing */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
--spacing-2xl: 3rem;
/* Border Radius */
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-full: 9999px;
/* Shadows */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
/* Transitions */
--transition-fast: 150ms ease;
--transition-normal: 200ms ease;
/* Z-index layers */
--z-bottom-nav: 1000;
--z-overlay: 1100;
--z-modal: 1200;
/* Bottom Nav Height */
--bottom-nav-height: 60px;
}
/* Reset and Base Styles */
*, *::before, *::after {
box-sizing: border-box;
}
html {
font-size: 16px;
-webkit-text-size-adjust: 100%;
}
body {
background-color: var(--bg-body);
color: var(--text-primary);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0;
line-height: 1.5;
/* Add bottom padding for mobile navigation */
padding-bottom: var(--bottom-nav-height);
}
/* Desktop Navigation (hidden on mobile) */
.main-nav {
background-color: var(--bg-nav);
color: var(--text-on-primary);
padding: var(--spacing-md) var(--spacing-xl);
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow-md);
position: sticky;
top: 0;
z-index: var(--z-bottom-nav);
}
@media (max-width: 768px) {
.main-nav {
display: none;
}
}
.nav-brand a {
color: var(--text-on-primary);
text-decoration: none;
font-size: 1.5rem;
font-weight: bold;
}
.nav-links {
display: flex;
gap: var(--spacing-lg);
}
.nav-links a {
color: var(--text-on-primary);
text-decoration: none;
opacity: 0.9;
transition: opacity var(--transition-fast);
padding: var(--spacing-sm) 0;
}
.nav-links a:hover {
opacity: 1;
}
.nav-user {
display: flex;
gap: var(--spacing-md);
align-items: center;
}
.nav-user span {
opacity: 0.8;
}
.nav-user a {
color: var(--text-on-primary);
text-decoration: none;
opacity: 0.9;
transition: opacity var(--transition-fast);
}
.nav-user a:hover {
opacity: 1;
}
/* Bottom Navigation (Mobile) */
.bottom-nav {
display: none;
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: var(--bottom-nav-height);
background-color: var(--bg-bottom-nav);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
z-index: var(--z-bottom-nav);
}
@media (max-width: 768px) {
.bottom-nav {
display: flex;
justify-content: space-around;
align-items: center;
}
}
.bottom-nav-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: var(--spacing-sm);
color: var(--text-on-primary);
text-decoration: none;
opacity: 0.7;
transition: opacity var(--transition-fast);
flex: 1;
}
.bottom-nav-item:hover,
.bottom-nav-item.active {
opacity: 1;
}
.bottom-nav-item svg {
width: 24px;
height: 24px;
margin-bottom: 2px;
}
.bottom-nav-item span {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Offline Notification */
.offline-notification {
position: fixed;
top: var(--spacing-md);
left: 50%;
transform: translateX(-50%);
background-color: var(--color-secondary);
color: var(--text-on-secondary);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-full);
display: flex;
align-items: center;
gap: var(--spacing-sm);
box-shadow: var(--shadow-md);
z-index: var(--z-overlay);
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
transform: translateX(-50%) translateY(-100%);
opacity: 0;
}
to {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
}
/* Bottom Nav Active State */
.bottom-nav-item.active {
opacity: 1;
color: var(--color-secondary);
}
.bottom-nav-item.active svg {
stroke: var(--color-secondary);
}
/* Main Content */
.main-content {
max-width: 1200px;
margin: 0 auto;
padding: var(--spacing-xl);
}
/* Responsive Main Content */
@media (max-width: 768px) {
.main-content {
padding: var(--spacing-md);
padding-bottom: calc(var(--spacing-xl) + var(--bottom-nav-height));
}
}
@media (max-width: 480px) {
.main-content {
padding: var(--spacing-sm);
padding-bottom: calc(var(--spacing-lg) + var(--bottom-nav-height));
}
}
/* Tables */
table {
width: 100%;
border-collapse: collapse;
margin: 1rem 0;
background: var(--bg-card);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
}
th, td {
padding: var(--spacing-md);
text-align: left;
border-bottom: 1px solid var(--color-gray-200);
}
th {
background-color: var(--color-gray-100);
font-weight: 600;
color: var(--text-primary);
}
tr:hover {
background-color: var(--color-gray-50);
}
/* Responsive Table - Convert to Cards on Mobile */
@media (max-width: 768px) {
/* Hide table headers but keep content accessible */
table thead {
display: none;
}
table, tbody, tr, td {
display: block;
width: 100%;
}
tr {
margin-bottom: var(--spacing-md);
border: 1px solid var(--color-gray-200);
border-radius: var(--radius-md);
background: var(--bg-card);
box-shadow: var(--shadow-sm);
}
td {
text-align: right;
padding-left: 50%;
position: relative;
border-bottom: none;
}
td::before {
content: attr(data-label);
position: absolute;
left: var(--spacing-md);
width: 45%;
font-weight: 600;
text-align: left;
color: var(--text-secondary);
}
td:last-child {
border-bottom: none;
}
}
/* Links */
a {
color: #2d5a27;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Headings */
h1, h2, h3 {
color: #2d5a27;
}
/* Buttons */
button, .btn {
background-color: #2d5a27;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
display: inline-block;
}
button:hover, .btn:hover {
background-color: #1e3d1a;
}
/* Forms */
input, select, textarea {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
}
input:focus, select:focus, textarea:focus {
outline: none;
border-color: #2d5a27;
}
/* Profile Stats Grid */
.profile-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 1rem 0;
}
.stat {
background: #f9f9f9;
padding: 1rem;
border-radius: 8px;
text-align: center;
}
.stat .label {
display: block;
font-size: 0.875rem;
color: #666;
margin-bottom: 0.5rem;
}
.stat .value {
font-size: 1.5rem;
font-weight: bold;
color: #2d5a27;
}
/* Partnership Table */
.partnerships-table {
margin: 1rem 0;
}
.confidence-high {
color: #2d5a27;
}
.confidence-medium {
color: #c9a227;
}
.confidence-low {
color: #999;
}
/* Game Cards */
.recent-games {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.game {
padding: 1rem;
border-radius: 8px;
border-left: 4px solid #999;
}
.game.won {
background-color: #e8f5e9;
border-left-color: #2d5a27;
}
.game.lost {
background-color: #ffebee;
border-left-color: #c62828;
}
.game-date {
font-size: 0.875rem;
color: #666;
margin-bottom: 0.5rem;
}
.game-teams {
display: flex;
align-items: center;
gap: 1rem;
font-weight: 500;
}
.game-score {
font-weight: bold;
color: #333;
}
.game-partner {
font-size: 0.875rem;
color: #666;
margin-top: 0.5rem;
}
/* Schedule Styles */
.schedule-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
section {
margin-bottom: 2rem;
}
section h2 {
border-bottom: 2px solid #2d5a27;
padding-bottom: 0.5rem;
margin-bottom: 1rem;
}
/* Match Cards */
.matches-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.match-card {
display: flex;
align-items: center;
padding: 1rem;
background: #f9f9f9;
border-radius: 8px;
gap: 1rem;
}
.match-date {
min-width: 120px;
font-size: 0.875rem;
color: #666;
}
.match-details {
flex: 1;
}
.teams {
display: flex;
align-items: center;
gap: 1rem;
}
.team {
padding: 0.5rem 1rem;
border-radius: 4px;
}
.your-team {
background-color: #e8f5e9;
color: #2d5a27;
font-weight: 500;
}
.opponent-team {
background-color: #f5f5f5;
}
.vs {
color: #999;
font-size: 0.875rem;
}
.match-actions {
min-width: 120px;
text-align: right;
}
.btn-small {
padding: 0.25rem 0.75rem;
font-size: 0.875rem;
}
/* Tournament Cards */
.tournaments-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1rem;
}
@media (max-width: 768px) {
.tournaments-list {
grid-template-columns: 1fr;
}
}
.tournament-card {
display: flex;
align-items: center;
padding: 1rem;
background: var(--bg-card-alt);
border-radius: var(--radius-md);
gap: 1rem;
border: 1px solid var(--color-gray-200);
}
@media (max-width: 480px) {
.tournament-card {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-sm);
}
.tournament-status {
align-self: flex-end;
}
}
.tournament-info {
flex: 1;
}
.tournament-info h3 {
margin: 0 0 0.25rem 0;
font-size: 1rem;
}
.tournament-format {
margin: 0;
color: #666;
font-size: 0.875rem;
}
.tournament-status {
min-width: 80px;
}
.status-badge {
padding: 0.25rem 0.5rem;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
}
.status-badge.active {
background-color: #e8f5e9;
color: #2d5a27;
}
.status-badge.pending {
background-color: #fff3e0;
color: #e65100;
}
/* Schedule by Tournament */
.schedule-by-tournament {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.tournament-schedule-item {
background: #f9f9f9;
padding: 1rem;
border-radius: 8px;
}
.tournament-schedule-item h3 {
margin: 0 0 1rem 0;
font-size: 1.1rem;
}
.rounds {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.round {
display: flex;
flex-direction: column;
padding: 0.5rem 1rem;
background: white;
border-radius: 4px;
border: 1px solid #ddd;
}
.round-number {
font-weight: 500;
}
.round-status {
font-size: 0.75rem;
color: #666;
text-transform: capitalize;
}
/* No matches/tournaments message */
.no-matches,
.no-tournaments,
.no-schedule {
color: var(--text-secondary);
font-style: italic;
padding: var(--spacing-xl);
text-align: center;
background: var(--bg-card-alt);
border-radius: var(--radius-lg);
}
/* Card Container */
.card-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--spacing-lg);
margin: var(--spacing-lg) 0;
}
@media (max-width: 768px) {
.card-container {
grid-template-columns: 1fr;
}
}
/* Card Base Styles */
.card {
background: var(--bg-card);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--spacing-lg);
transition: box-shadow var(--transition-normal);
}
.card:hover {
box-shadow: var(--shadow-md);
}
/* Responsive visibility utilities */
.hide-mobile {
display: block;
}
.show-mobile {
display: none;
}
@media (max-width: 768px) {
.hide-mobile {
display: none !important;
}
.show-mobile {
display: block !important;
}
}
/* Form responsive styles */
.form-group input,
.form-group select,
.form-group textarea {
width: 100%;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-gray-300);
border-radius: var(--radius-sm);
font-size: 1rem;
transition: border-color var(--transition-fast);
}
@media (max-width: 480px) {
.form-group input,
.form-group select,
.form-group textarea {
padding: var(--spacing-sm);
font-size: 16px; /* Prevents zoom on iOS */
}
}
/* Dashboard Stats */
.dashboard-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
@media (max-width: 480px) {
.dashboard-stats {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-sm);
}
}
.stat-card {
background: white;
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 1.5rem;
text-align: center;
transition: box-shadow 0.2s;
}
.stat-card:hover {
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.stat-value {
font-size: 2.5rem;
font-weight: bold;
color: #2d5a27;
margin-bottom: 0.5rem;
}
.stat-label {
color: #666;
margin-bottom: 1rem;
}
.stat-link {
display: inline-block;
font-size: 0.9rem;
color: #2d5a27;
text-decoration: none;
}
.stat-link:hover {
text-decoration: underline;
}
/* Quick Actions */
.quick-actions {
margin: 2rem 0;
}
.action-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
margin-top: 1rem;
}
.action-buttons .btn {
padding: 0.75rem 1.5rem;
}
/* Recent Activity */
.recent-activity {
margin: 2rem 0;
}
/* Admin Links */
.admin-links ul {
list-style: none;
padding: 0;
}
.admin-links li {
padding: 0.75rem 0;
border-bottom: 1px solid #eee;
}
.admin-links a {
font-weight: 500;
color: #2d5a27;
}
/* Forms */
.player-form {
max-width: 500px;
margin: 2rem 0;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
.form-group input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
}
.form-group input:focus {
outline: none;
border-color: #2d5a27;
box-shadow: 0 0 0 2px rgba(45, 90, 39, 0.2);
}
.form-group small {
color: #666;
font-size: 0.875rem;
}
.form-actions {
display: flex;
gap: 1rem;
margin-top: 2rem;
}
/* Auth Pages */
.auth-container {
display: flex;
justify-content: center;
align-items: center;
min-height: calc(100vh - 200px);
padding: 2rem;
}
.auth-card {
background: white;
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.auth-card h1 {
text-align: center;
margin-bottom: 1.5rem;
color: #2d5a27;
}
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-group input[type="email"],
.form-group input[type="password"],
.form-group input[type="text"] {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 1rem;
box-sizing: border-box;
}
.form-group input:focus {
outline: none;
border-color: #2d5a27;
box-shadow: 0 0 0 2px rgba(45, 90, 39, 0.2);
}
.remember-me label {
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: normal;
cursor: pointer;
}
.btn-block {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
margin-top: 0.5rem;
}
.btn-primary {
background-color: #2d5a27;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary:hover {
background-color: #1e3d1a;
}
.auth-links {
margin-top: 1.5rem;
text-align: center;
font-size: 0.9rem;
}
.auth-links p {
margin: 0.5rem 0;
}
/* Alerts */
.alert {
padding: 1rem;
border-radius: 6px;
margin-bottom: 1rem;
}
.alert-error {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ef9a9a;
}
.alert-success {
background-color: #e8f5e9;
color: #2d5a27;
border: 1px solid #a5d6a7;
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

-118
View File
@@ -1,118 +0,0 @@
import "../css/app.css";
// EuchreCamp Application JavaScript
// Initialize the application
document.addEventListener('DOMContentLoaded', () => {
initBottomNav();
initOfflineSupport();
initThemePreference();
});
// Bottom Navigation Active State
function initBottomNav() {
const currentPath = window.location.pathname;
const navItems = document.querySelectorAll('.bottom-nav-item');
navItems.forEach(item => {
if (item.getAttribute('href') === currentPath) {
item.classList.add('active');
}
});
}
// Offline Support with Local Storage
function initOfflineSupport() {
// Cache recent data for offline access
if ('caches' in window) {
// Listen for online/offline events
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// Check initial state
if (!navigator.onLine) {
handleOffline();
}
}
}
function handleOnline() {
console.log('App is online');
// Clear any offline indicators
document.body.classList.remove('offline');
// Sync any pending changes
syncPendingChanges();
}
function handleOffline() {
console.log('App is offline');
// Add offline indicator to body
document.body.classList.add('offline');
// Show offline notification
showOfflineNotification();
}
function showOfflineNotification() {
// Create or update offline notification
let notification = document.getElementById('offline-notification');
if (!notification) {
notification = document.createElement('div');
notification.id = 'offline-notification';
notification.className = 'offline-notification';
notification.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" width="20" height="20">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 5.636a9 9 0 010 12.728m0 0l-2.829-2.829m2.829 2.829L21 21M15.536 8.464a5 5 0 010 7.072m0 0l-2.829-2.829m-4.243 2.829a4.978 4.978 0 01-1.414-2.83m-1.414 5.658a9 9 0 01-2.167-9.238m7.824 2.167a1 1 0 111.414 1.414m-1.414-1.414L3 3m8.293 8.293l1.414 1.414" />
</svg>
<span>Offline Mode - Some features may be limited</span>
`;
document.body.appendChild(notification);
}
}
function syncPendingChanges() {
// Implement sync logic here
// This would sync any locally stored changes to the server
console.log('Syncing pending changes...');
}
// Theme Preference
function initThemePreference() {
// Check for saved theme preference
const savedTheme = localStorage.getItem('euchrecamp-theme');
if (savedTheme) {
document.documentElement.setAttribute('data-theme', savedTheme);
}
}
// Utility Functions
function saveToLocalStorage(key, data) {
try {
localStorage.setItem(`euchrecamp-${key}`, JSON.stringify(data));
return true;
} catch (e) {
console.error('Error saving to localStorage:', e);
return false;
}
}
function getFromLocalStorage(key) {
try {
const data = localStorage.getItem(`euchrecamp-${key}`);
return data ? JSON.parse(data) : null;
} catch (e) {
console.error('Error reading from localStorage:', e);
return null;
}
}
function clearLocalStorage(key) {
try {
localStorage.removeItem(`euchrecamp-${key}`);
return true;
} catch (e) {
console.error('Error clearing localStorage:', e);
return false;
}
}
-75
View File
@@ -1,75 +0,0 @@
# frozen_string_literal: true
require "good_job"
module EuchreCamp
module Jobs
class CalculateEloJob < ActiveJob::Base
queue_as :default
# This job calculates Elo ratings for a match and updates player records
def perform(match_id)
match = find_match(match_id)
return unless match
# Get current player ratings
player_ratings = fetch_player_ratings(match)
# Calculate new ratings
calculation_results = EuchreCamp::Services::EloCalculator.calculate(
player_ratings: player_ratings,
team_1_players: [match[:team_1_p1_id], match[:team_1_p2_id]],
team_2_players: [match[:team_2_p1_id], match[:team_2_p2_id]],
team_1_score: match[:team_1_score],
team_2_score: match[:team_2_score]
)
# Update player ratings and create snapshots
update_player_ratings(calculation_results, match_id)
# Record partnerships (after Elo snapshots are created)
EuchreCamp::Services::PartnershipTracker.record_match(match_id)
end
private
def find_match(match_id)
rom = EuchreCamp::App["persistence.rom"]
rom.relations[:matches].by_pk(match_id).one
end
def fetch_player_ratings(match)
rom = EuchreCamp::App["persistence.rom"]
player_ids = [
match[:team_1_p1_id],
match[:team_1_p2_id],
match[:team_2_p1_id],
match[:team_2_p2_id]
]
players = rom.relations[:players].where(id: player_ids).to_a
players.each_with_object({}) do |player, hash|
hash[player[:id]] = player[:current_elo] || 1000
end
end
def update_player_ratings(results, match_id)
rom = EuchreCamp::App["persistence.rom"]
results.each do |player_id, data|
# Update player's current Elo
rom.relations[:players].where(id: player_id).update(current_elo: data[:rating_after])
# Create Elo snapshot record
rom.relations[:elo_snapshots].insert(
player_id: player_id,
match_id: match_id,
rating_before: data[:rating_before],
rating_after: data[:rating_after],
rating_change: data[:rating_change]
)
end
end
end
end
end
-26
View File
@@ -1,26 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class BracketMatchups < ::EuchreCamp::Repository[:bracket_matchups]
commands :create, update: :by_pk, delete: :by_pk
def by_event(event_id)
bracket_matchups.where(event_id: event_id).to_a
end
def by_round(round_id)
bracket_matchups.where(round_id: round_id).order(:bracket_position).to_a
end
def by_event_and_round(event_id, round_id)
bracket_matchups
.where(event_id: event_id, round_id: round_id)
.order(:bracket_position)
.to_a
end
end
end
end
-39
View File
@@ -1,39 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class EventParticipants < ::EuchreCamp::Repository[:event_participants]
commands :create, update: :by_pk, delete: :by_pk
def by_event(event_id)
event_participants.where(event_id: event_id).to_a
end
def players_by_event(event_id)
# Return player IDs registered for an event
event_participants
.where(event_id: event_id)
.select(:player_id)
.to_a
.map { |p| p[:player_id] }
end
def register_player(event_id, player_id, seed: nil)
create(
event_id: event_id,
player_id: player_id,
status: 'registered',
seed: seed
)
end
def remove_player(event_id, player_id)
event_participants
.where(event_id: event_id, player_id: player_id)
.delete
end
end
end
end
-27
View File
@@ -1,27 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class Events < ::EuchreCamp::Repository[:events]
commands :create, update: :by_pk, delete: :by_pk
def all
events.to_a
end
def by_id(id)
events.by_pk(id).one!
end
def upcoming
events.where(status: ['planned', 'active']).order(:event_date).to_a
end
def completed
events.where(status: 'completed').order(Sequel.desc(:event_date)).to_a
end
end
end
end
-19
View File
@@ -1,19 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class Matches < ::EuchreCamp::Repository[:matches]
commands :create, update: :by_pk, delete: :by_pk
def all
matches.to_a
end
def by_id(id)
matches.by_pk(id).one!
end
end
end
end
-30
View File
@@ -1,30 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class PartnershipGames < ::EuchreCamp::Repository[:partnership_games]
commands :create, update: :by_pk, delete: :by_pk
def by_player(player_id)
partnership_games.where(
Sequel.|({ player_1_id: player_id }, { player_2_id: player_id })
).to_a
end
def by_match(match_id)
partnership_games.where(match_id: match_id).to_a
end
def by_partnership(player_1_id, player_2_id)
partnership_games.where(
Sequel.|(
{ player_1_id: player_1_id, player_2_id: player_2_id },
{ player_1_id: player_2_id, player_2_id: player_1_id }
)
).to_a
end
end
end
end
-51
View File
@@ -1,51 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class PartnershipStats < ::EuchreCamp::Repository[:partnership_stats]
commands :create, update: :by_pk
def find_or_create(player_1_id, player_2_id)
# Ensure consistent ordering (lower ID first)
p1_id = [player_1_id, player_2_id].min
p2_id = [player_1_id, player_2_id].max
stats = partnership_stats.where(player_1_id: p1_id, player_2_id: p2_id).one
return stats if stats
create(
player_1_id: p1_id,
player_2_id: p2_id,
games_played: 0,
wins: 0,
losses: 0,
total_elo_change: 0
)
end
def by_player(player_id)
partnership_stats.where(
Sequel.|({ player_1_id: player_id }, { player_2_id: player_id })
).to_a
end
def update_stats(player_1_id, player_2_id, won: false, elo_change: 0)
stats = find_or_create(player_1_id, player_2_id)
update_params = {
games_played: stats.games_played + 1,
wins: stats.wins + (won ? 1 : 0),
losses: stats.losses + (won ? 0 : 1),
total_elo_change: stats.total_elo_change + elo_change,
last_played: Time.now,
updated_at: Time.now
}
update(stats.id, update_params)
end
end
end
end
-17
View File
@@ -1,17 +0,0 @@
require 'rom-repository'
module EuchreCamp
module Repositories
class Players < ::EuchreCamp::Repository[:players]
commands :create, update: :by_pk, delete: :by_pk
def all
players.to_a
end
def by_id(id)
players.by_pk(id).one!
end
end
end
end
-32
View File
@@ -1,32 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class Teams < ::EuchreCamp::Repository[:teams]
commands :create, update: :by_pk, delete: :by_pk
def by_event(event_id)
teams.where(event_id: event_id).to_a
end
def find_or_create(event_id, player_1_id, player_2_id, team_name = nil)
existing = teams.where(
event_id: event_id,
player_1_id: [player_1_id, player_2_id],
player_2_id: [player_1_id, player_2_id]
).one
return existing if existing
create(
event_id: event_id,
player_1_id: player_1_id,
player_2_id: player_2_id,
team_name: team_name || "Team #{player_1_id}-#{player_2_id}"
)
end
end
end
end
-30
View File
@@ -1,30 +0,0 @@
# frozen_string_literal: true
require 'rom-repository'
module EuchreCamp
module Repositories
class TournamentRounds < ::EuchreCamp::Repository[:tournament_rounds]
commands :create, update: :by_pk, delete: :by_pk
def by_event(event_id)
tournament_rounds.where(event_id: event_id).order(:round_number).to_a
end
def current_round(event_id)
tournament_rounds
.where(event_id: event_id, status: 'active')
.order(Sequel.desc(:round_number))
.one
end
def next_round(event_id)
tournament_rounds
.where(event_id: event_id, status: 'pending')
.order(:round_number)
.limit(1)
.one
end
end
end
end
-118
View File
@@ -1,118 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Repositories
class Users < ::EuchreCamp::Repository[:users]
# Find user by ID
def by_id(id)
users.by_pk(id).one
end
# Find user by email
def by_email(email)
users.where(email: email).one
end
# Find user by player ID
def by_player_id(player_id)
users.where(player_id: player_id).one
end
# Create new user
def create(email:, password_digest:, player_id:, role: 'player', confirmed: false)
users.changeset(:create, {
email: email,
password_digest: password_digest,
player_id: player_id,
role: role,
confirmed: confirmed,
confirmation_token: confirmed ? nil : generate_token,
confirmation_sent_at: confirmed ? nil : Time.now
}).commit
end
# Confirm user account
def confirm(token)
user = users.where(confirmation_token: token).one
return nil unless user
users.where(id: user[:id]).update(
confirmed: true,
confirmation_token: nil,
confirmation_sent_at: nil
)
user
end
# Request password reset
def request_password_reset(email)
user = by_email(email)
return nil unless user
token = generate_token
users.where(id: user[:id]).update(
reset_password_token: token,
reset_password_sent_at: Time.now
)
token
end
# Reset password
def reset_password(token, new_password_digest)
user = users.where(reset_password_token: token).one
return nil unless user
# Check if token is expired (1 hour)
if user[:reset_password_sent_at] && user[:reset_password_sent_at] < (Time.now - 3600)
return nil
end
users.where(id: user[:id]).update(
password_digest: new_password_digest,
reset_password_token: nil,
reset_password_sent_at: nil
)
user
end
# Record successful login
def record_login(user_id, ip_address)
users.where(id: user_id).update(
last_login_at: Time.now,
last_login_ip: ip_address,
failed_login_attempts: 0,
locked_until: nil
)
end
# Record failed login
def record_failed_login(user_id)
user = users.where(id: user_id).one
return unless user
attempts = user[:failed_login_attempts] + 1
locked_until = attempts >= 5 ? (Time.now + 900) : nil # Lock for 15 minutes after 5 attempts
users.where(id: user_id).update(
failed_login_attempts: attempts,
locked_until: locked_until
)
end
# Check if account is locked
def account_locked?(user_id)
user = users.where(id: user_id).one
return false unless user
user[:locked_until] && user[:locked_until] > Time.now
end
private
def generate_token
SecureRandom.urlsafe_base64(32)
end
end
end
end
-7
View File
@@ -1,7 +0,0 @@
require 'rom-repository'
module EuchreCamp
class Repository < ROM::Repository::Root
include Deps[container: 'persistence.rom']
end
end
-136
View File
@@ -1,136 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Services
class Authorization
# Permission matrix for different user roles
PERMISSIONS = {
player: {
edit_own_profile: true,
record_own_matches: true,
edit_own_matches_within_5_min: true,
view_all: true,
create_tournament: false,
manage_tournament: false,
edit_any_record: false,
delete_any_record: false
},
tournament_admin: {
edit_own_profile: true,
record_own_matches: true,
edit_own_matches_within_5_min: true,
view_all: true,
create_tournament: true,
manage_tournament: true,
edit_any_record: true, # Within their tournaments
delete_any_record: false
},
club_admin: {
edit_own_profile: true,
record_own_matches: true,
edit_own_matches_within_5_min: true,
view_all: true,
create_tournament: true,
manage_tournament: true,
edit_any_record: true,
delete_any_record: true
},
system_admin: {
edit_own_profile: true,
record_own_matches: true,
edit_own_matches_within_5_min: true,
view_all: true,
create_tournament: true,
manage_tournament: true,
edit_any_record: true,
delete_any_record: true
}
}
def self.can?(user, action, resource = nil, context = {})
return false unless user
role_sym = user.role.to_sym
permissions = PERMISSIONS[role_sym] || PERMISSIONS[:player]
case action
when :edit_own_profile
# User can edit their own profile
resource.nil? || resource[:id] == user.player_id
when :edit_own_match_within_5_min
# User can edit match if it's their match and within 5 minutes
return false unless resource
return false unless match_within_timeframe?(resource, 5)
# Check if user is in the match
user_in_match = [
resource[:team_1_p1_id],
resource[:team_1_p2_id],
resource[:team_2_p1_id],
resource[:team_2_p2_id]
].include?(user.player_id)
user_in_match || permissions[:edit_any_record]
when :manage_tournament
# Tournament admin can manage their tournaments
# Club admin can manage all tournaments
return true if permissions[:club_admin?] || permissions[:club_admin?]
# Check if user is tournament admin for this tournament
if resource && user.tournament_admin?
# In a real implementation, check tournament.admin_id == user.player_id
true
else
false
end
when :edit_any_record, :delete_any_record
permissions[:edit_any_record] || permissions[:delete_any_record]
when :view_admin_panel
permissions[:create_tournament] || permissions[:manage_tournament]
else
false
end
end
def self.match_within_timeframe?(match, minutes)
return false unless match[:played_at] || match[:created_at]
timestamp = match[:played_at] || match[:created_at]
time_diff = Time.now - timestamp
time_diff <= (minutes * 60)
end
def self.get_accessible_tournaments(user)
rom = EuchreCamp::App["persistence.rom"]
if user.club_admin? || user.role == 'system_admin'
# Club admin can see all tournaments
rom.relations[:events].to_a
elsif user.tournament_admin?
# Tournament admin can see tournaments they administer
# In a real app, this would query by admin_id
rom.relations[:events].to_a
else
# Regular player can see tournaments they participate in
# Get tournaments where user is a participant
participant_ids = rom.relations[:event_participants]
.where(player_id: user.player_id)
.select(:event_id)
.to_a
.map { |p| p[:event_id] }
if participant_ids.any?
rom.relations[:events].where(id: participant_ids).to_a
else
[]
end
end
end
end
end
end
-81
View File
@@ -1,81 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Services
class EloCalculator
# Standard Elo K-factor
K_FACTOR = 32
# Calculate Elo ratings for a 2v2 Euchre match
#
# @param player_ratings [Hash] player_id => current_rating
# @param team_1_players [Array<Integer>] player IDs for team 1
# @param team_2_players [Array<Integer>] player IDs for team 2
# @param team_1_score [Integer] score for team 1
# @param team_2_score [Integer] score for team 2
# @return [Hash] player_id => { rating_before:, rating_after:, rating_change: }
def self.calculate(player_ratings:, team_1_players:, team_2_players:, team_1_score:, team_2_score:)
# Validate inputs
raise ArgumentError, "Team 1 must have exactly 2 players" unless team_1_players.length == 2
raise ArgumentError, "Team 2 must have exactly 2 players" unless team_2_players.length == 2
# Determine winner (first to 10 points)
# Allow for exact score tie at 10-10 if provided
team_1_wins = team_1_score > team_2_score && team_1_score >= 10
team_2_wins = team_2_score > team_1_score && team_2_score >= 10
unless team_1_wins || team_2_wins
raise ArgumentError, "One team must reach 10 points to win (Team 1: #{team_1_score}, Team 2: #{team_2_score})"
end
if team_1_wins && team_2_wins
raise ArgumentError, "Only one team can win (both reached 10 points)"
end
winner = team_1_wins ? :team_1 : :team_2
# Calculate average ratings for each team
team_1_avg = team_1_players.map { |id| player_ratings[id] }.sum / 2.0
team_2_avg = team_2_players.map { |id| player_ratings[id] }.sum / 2.0
# Calculate expected scores using Elo formula
# E = 1 / (1 + 10^((R_opp - R_team) / 400))
expected_team_1 = 1.0 / (1.0 + 10.0**((team_2_avg - team_1_avg) / 400.0))
expected_team_2 = 1.0 / (1.0 + 10.0**((team_1_avg - team_2_avg) / 400.0))
# Actual scores (1 for win, 0 for loss)
actual_team_1 = team_1_wins ? 1.0 : 0.0
actual_team_2 = team_2_wins ? 1.0 : 0.0
# Calculate rating changes for each player
results = {}
# Team 1 players
team_1_players.each do |player_id|
old_rating = player_ratings[player_id]
rating_change = K_FACTOR * (actual_team_1 - expected_team_1)
new_rating = (old_rating + rating_change).round
results[player_id] = {
rating_before: old_rating,
rating_after: new_rating,
rating_change: rating_change.round
}
end
# Team 2 players
team_2_players.each do |player_id|
old_rating = player_ratings[player_id]
rating_change = K_FACTOR * (actual_team_2 - expected_team_2)
new_rating = (old_rating + rating_change).round
results[player_id] = {
rating_before: old_rating,
rating_after: new_rating,
rating_change: rating_change.round
}
end
results
end
end
end
end
-108
View File
@@ -1,108 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Services
class PartnershipTracker
# Record partnerships when a match completes
def self.record_match(match_id)
rom = EuchreCamp::App["persistence.rom"]
match = rom.relations[:matches].by_pk(match_id).one
return unless match
games_repo = EuchreCamp::App["repositories.partnership_games"]
stats_repo = EuchreCamp::App["repositories.partnership_stats"]
# Calculate Elo changes for each player
# This is called AFTER Elo calculation, so we get the change from elo_snapshots
elo_changes = calculate_elo_changes(match_id)
# Record Team 1 partnership
record_partnership(
games_repo: games_repo,
stats_repo: stats_repo,
match_id: match_id,
player_1_id: match[:team_1_p1_id],
player_2_id: match[:team_1_p2_id],
team_number: 1,
won: match[:team_1_score] > match[:team_2_score],
elo_change: elo_changes[match[:team_1_p1_id]] + elo_changes[match[:team_1_p2_id]]
)
# Record Team 2 partnership
record_partnership(
games_repo: games_repo,
stats_repo: stats_repo,
match_id: match_id,
player_1_id: match[:team_2_p1_id],
player_2_id: match[:team_2_p2_id],
team_number: 2,
won: match[:team_2_score] > match[:team_1_score],
elo_change: elo_changes[match[:team_2_p1_id]] + elo_changes[match[:team_2_p2_id]]
)
end
def self.calculate_elo_changes(match_id)
rom = EuchreCamp::App["persistence.rom"]
# Get the two most recent snapshots before and after this match
snapshots = rom.relations[:elo_snapshots]
.where(match_id: match_id)
.order(:created_at)
.to_a
# Group by player and calculate change
changes = Hash.new(0)
snapshots.group_by { |s| s[:player_id] }.each do |player_id, player_snapshots|
# For a single match, there should be exactly one snapshot per player
# The snapshot contains rating_before and rating_after for this specific match
# So we calculate the change directly from that snapshot
player_snapshots.each do |snapshot|
changes[player_id] = snapshot[:rating_after] - snapshot[:rating_before]
end
end
changes
end
def self.record_partnership(games_repo:, stats_repo:, match_id:, player_1_id:, player_2_id:, team_number:, won:, elo_change:)
# Record the partnership game
games_repo.create(
match_id: match_id,
player_1_id: player_1_id,
player_2_id: player_2_id,
team_number: team_number,
won_match: won ? 1 : 0
)
# Update partnership statistics (using consistent ordering)
stats_repo.update_stats(player_1_id, player_2_id, won: won, elo_change: elo_change)
end
# Get partnership statistics for a player
def self.get_player_partnerships(player_id)
stats_repo = EuchreCamp::App["repositories.partnership_stats"]
players_repo = EuchreCamp::App["repositories.players"]
stats = stats_repo.by_player(player_id)
all_players = players_repo.all.to_h { |p| [p[:id], p] }
stats.map do |stat|
partner_id = stat[:player_1_id] == player_id ? stat[:player_2_id] : stat[:player_1_id]
partner = all_players[partner_id]
{
partner: partner,
games_played: stat[:games_played],
wins: stat[:wins],
losses: stat[:losses],
win_rate: stat[:games_played] > 0 ? (stat[:wins].to_f / stat[:games_played]).round(3) : 0,
total_elo_change: stat[:total_elo_change],
avg_elo_change: stat[:games_played] > 0 ? (stat[:total_elo_change].to_f / stat[:games_played]).round(2) : 0,
last_played: stat[:last_played]
}
end.sort_by { |p| -p[:games_played] }
end
end
end
end
-169
View File
@@ -1,169 +0,0 @@
# frozen_string_literal: true
require "csv"
module EuchreCamp
module Services
class TournamentCsvImporter
# Map table names to numeric IDs
TABLE_NAME_MAP = {
"Clubs" => 1,
"Hearts" => 2,
"Diamonds" => 3,
"Spades" => 4,
"Stars" => 5
}.freeze
def initialize(event_id:)
@event_id = event_id
@rom = EuchreCamp::App["persistence.rom"]
@matches_repo = EuchreCamp::App["repositories.matches"]
@teams_repo = EuchreCamp::App["repositories.teams"]
@events_repo = EuchreCamp::App["repositories.events"]
@rounds_repo = EuchreCamp::App["repositories.tournament_rounds"]
@bracket_matchups_repo = EuchreCamp::App["repositories.bracket_matchups"]
end
def import(csv_file_path)
results = {
matches_created: 0,
teams_created: 0,
rounds_created: 0,
errors: []
}
begin
event = @events_repo.by_id(@event_id)
CSV.foreach(csv_file_path, headers: true, header_converters: :symbol) do |row|
begin
process_row(row, event, results)
rescue => e
results[:errors] << "Row #{row}: #{e.message}"
end
end
rescue => e
results[:errors] << "Fatal error: #{e.message}"
end
results
end
private
def process_row(row, event, results)
# Extract data from CSV row
round_number = row[:round].to_i
table_name = row[:table]
seat_1_name = row[:seat_1]&.strip
seat_3_name = row[:seat_3]&.strip
odds_points = row[:odds_points].to_i
seat_2_name = row[:seat_2]&.strip
seat_4_name = row[:seat_4]&.strip
evens_points = row[:evens_points].to_i
winner = row[:winner]&.strip&.downcase
# Validate required fields
unless seat_1_name && seat_3_name && seat_2_name && seat_4_name
raise "Missing player names in row"
end
# Find or create players
player_1 = find_or_create_player(seat_1_name)
player_2 = find_or_create_player(seat_3_name)
player_3 = find_or_create_player(seat_2_name)
player_4 = find_or_create_player(seat_4_name)
# Find or create teams
team_1 = @teams_repo.find_or_create(@event_id, player_1[:id], player_2[:id], "#{seat_1_name} + #{seat_3_name}")
team_2 = @teams_repo.find_or_create(@event_id, player_3[:id], player_4[:id], "#{seat_2_name} + #{seat_4_name}")
results[:teams_created] += 1 if team_1[:created_at] == team_2[:created_at] # Rough check for new teams
# Find or create round
round = find_or_create_round(round_number)
# Map table name to number
table_number = TABLE_NAME_MAP[table_name] || table_name.to_i
if table_number == 0
raise "Unknown table name: #{table_name}"
end
# Determine winner based on scores or explicit winner column
team_1_score = odds_points
team_2_score = evens_points
# Verify winner matches scores if explicitly stated
if winner
if winner == "odds" && team_1_score <= team_2_score
results[:errors] << "Warning: Row claims Odds won but score is #{team_1_score}-#{team_2_score}"
elsif winner == "evens" && team_2_score <= team_1_score
results[:errors] << "Warning: Row claims Evens won but score is #{team_1_score}-#{team_2_score}"
end
end
# Create match
match_data = {
team_1_p1_id: player_1[:id],
team_1_p2_id: player_2[:id],
team_1_score: team_1_score,
team_2_p1_id: player_3[:id],
team_2_p2_id: player_4[:id],
team_2_score: team_2_score,
played_at: Time.now,
status: 'completed'
}
match = @matches_repo.create(match_data)
results[:matches_created] += 1
# Create bracket matchup entry
bracket_data = {
round_id: round[:id],
event_id: @event_id,
team_1_id: team_1[:id],
team_2_id: team_2[:id],
table_number: table_number,
bracket_position: table_number,
status: 'completed'
}
@bracket_matchups_repo.create(bracket_data)
# Queue Elo calculation
EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id])
rescue => e
raise "Error processing row: #{e.message}"
end
def find_or_create_player(name)
return nil unless name
player = @rom.relations[:players].where(name: name).one
if player
player
else
# Create player with default rating
@rom.relations[:players].insert(name: name, rating: 0, current_elo: 1000)
@rom.relations[:players].where(name: name).one
end
end
def find_or_create_round(round_number)
round = @rom.relations[:tournament_rounds].where(event_id: @event_id, round_number: round_number).one
if round
round
else
@rounds_repo.create(
event_id: @event_id,
round_number: round_number,
status: 'completed'
)
end
end
end
end
end
-204
View File
@@ -1,204 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Services
class TournamentGenerator
# Generate matchups for round-robin tournament
# Uses circle method for scheduling
def self.round_robin(teams)
n = teams.length
return [] if n < 2
# Handle odd number of teams by adding a bye
teams_with_bye = teams.dup
bye_team = { id: nil, name: "BYE" }
teams_with_bye << bye_team if n.odd?
total_teams = teams_with_bye.length
rounds = []
# Circle method: fix first team, rotate others
(total_teams - 1).times do |round_num|
round_matchups = []
half = total_teams / 2
half.times do |i|
team_a = teams_with_bye[i]
team_b = teams_with_bye[total_teams - 1 - i]
# Skip if either is BYE
next if team_a[:id].nil? || team_b[:id].nil?
round_matchups << {
team_1_id: team_a[:id],
team_2_id: team_b[:id]
}
end
rounds << {
round_number: round_num + 1,
matchups: round_matchups
}
# Rotate teams (circle method)
teams_with_bye = [teams_with_bye[0]] + teams_with_bye[1..-1].rotate(-1)
end
rounds
end
# Generate single elimination bracket
def self.single_elimination(seeded_teams)
# seeded_teams should be array of { id: team_id, seed: number }
teams = seeded_teams.sort_by { |t| t[:seed] || 0 }
# Ensure power of 2 or add byes
bracket_size = 1
while bracket_size < teams.length
bracket_size *= 2
end
# Create bracket structure
bracket = []
positions = {}
# Calculate bracket positions
(1..bracket_size).each do |pos|
positions[pos] = pos
end
# Fill bracket with teams and byes
team_index = 0
(1..bracket_size).each do |pos|
if team_index < teams.length
positions[pos] = teams[team_index][:id]
team_index += 1
else
positions[pos] = nil # BYE
end
end
# Generate matchups for first round
matchups = []
num_matchups = bracket_size / 2
num_matchups.times do |i|
pos1 = i * 2 + 1
pos2 = i * 2 + 2
team1 = positions[pos1]
team2 = positions[pos2]
# Only add matchup if both teams exist (no BYE vs BYE)
unless team1.nil? && team2.nil?
matchups << {
team_1_id: team1,
team_2_id: team2,
bracket_position: i + 1,
winner_position: (i / 2) + 1 # Where winner advances to
}
end
end
{
bracket_size: bracket_size,
first_round_matchups: matchups,
bracket_positions: positions
}
end
# Generate double elimination bracket
def self.double_elimination(seeded_teams)
# For simplicity, returning structure for winner's bracket and loser's bracket
teams = seeded_teams.sort_by { |t| t[:seed] || 0 }
# Generate winner's bracket (same as single elim)
winners_bracket = single_elimination(teams)
# Loser's bracket structure (simplified)
# In full implementation, this would be more complex
loser_bracket_rounds = []
{
winners_bracket: winners_bracket,
loser_bracket: loser_bracket_rounds
}
end
# Generate Swiss system pairings
def self.swiss(teams, round_number, standings = {})
# standings should be hash of team_id => { wins, losses, points }
return [] if teams.empty?
# Sort teams by current score (descending)
sorted_teams = teams.sort_by do |team|
score = standings[team[:id]] || { wins: 0, losses: 0, points: 0 }
[-score[:wins], -score[:points]]
end
# Pair similar scores
matchups = []
paired = Set.new
sorted_teams.each_with_index do |team, i|
next if paired.include?(team[:id])
# Find opponent with similar score
opponent = nil
(i + 1...sorted_teams.length).each do |j|
candidate = sorted_teams[j]
unless paired.include?(candidate[:id])
opponent = candidate
break
end
end
if opponent
matchups << {
team_1_id: team[:id],
team_2_id: opponent[:id]
}
paired << team[:id]
paired << opponent[:id]
end
end
[{
round_number: round_number,
matchups: matchups
}]
end
# Calculate number of matches for each format
def self.match_count(format, participant_count)
case format
when 'round_robin'
participant_count * (participant_count - 1) / 2
when 'single_elim'
participant_count - 1
when 'double_elim'
(participant_count * 2) - 2
when 'swiss'
# Assuming log2(participant_count) rounds
rounds = Math.log2(participant_count).ceil
(participant_count * rounds) / 2
else
0
end
end
# Validate participant count for format
def self.valid_participant_count?(format, count)
case format
when 'round_robin'
count >= 2
when 'single_elim', 'double_elim'
# Must be power of 2 for pure bracket, or allow byes
count >= 2
when 'swiss'
count >= 2
else
false
end
end
end
end
end
-91
View File
@@ -1,91 +0,0 @@
<h1>Admin Dashboard</h1>
<div class="dashboard-stats">
<div class="stat-card">
<div class="stat-value"><%= total_players %></div>
<div class="stat-label">Total Players</div>
<a href="/admin/players" class="stat-link">Manage Players →</a>
</div>
<div class="stat-card">
<div class="stat-value"><%= active_tournaments %></div>
<div class="stat-label">Active Tournaments</div>
<a href="/admin/tournaments" class="stat-link">Manage Tournaments →</a>
</div>
<div class="stat-card">
<div class="stat-value"><%= completed_tournaments %></div>
<div class="stat-label">Completed Tournaments</div>
<a href="/admin/tournaments" class="stat-link">View History →</a>
</div>
<div class="stat-card">
<div class="stat-value"><%= recent_matches.count %></div>
<div class="stat-label">Recent Matches</div>
<a href="/admin/matches" class="stat-link">Record Match →</a>
</div>
</div>
<hr>
<div class="quick-actions">
<h2>Quick Actions</h2>
<div class="action-buttons">
<% if defined?(current_user) && current_user %>
<a href="/admin/matches/new" class="btn btn-primary">Record Match Result</a>
<% if EuchreCamp::Services::Authorization.can?(current_user, :create_tournament) %>
<a href="/admin/tournaments/new" class="btn btn-primary">Create Tournament</a>
<% end %>
<% if EuchreCamp::Services::Authorization.can?(current_user, :edit_any_record) %>
<a href="/admin/players/new" class="btn btn-primary">Add Player</a>
<% end %>
<a href="/admin/matches/upload" class="btn">Upload CSV</a>
<% else %>
<a href="/admin/matches/new" class="btn btn-primary">Record Match Result</a>
<% end %>
</div>
</div>
<hr>
<div class="recent-activity">
<h2>Recent Matches</h2>
<% if recent_matches.any? %>
<table>
<thead>
<tr>
<th>Match ID</th>
<th>Team 1</th>
<th>Score</th>
<th>Team 2</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<% recent_matches.each do |match| %>
<tr>
<td data-label="Match ID"><%= match[:id] %></td>
<td data-label="Team 1"><%= match[:team_1].map { |p| p[:name] }.join(' + ') %></td>
<td data-label="Score"><strong><%= match[:score] %></strong></td>
<td data-label="Team 2"><%= match[:team_2].map { |p| p[:name] }.join(' + ') %></td>
<td data-label="Date"><%= match[:played_at]&.strftime('%Y-%m-%d') || '-' %></td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No matches recorded yet.</p>
<% end %>
</div>
<hr>
<div class="admin-links">
<h2>Admin Sections</h2>
<ul>
<li><a href="/admin/players">Player Management</a> - Add, edit, and manage players</li>
<li><a href="/admin/tournaments">Tournament Management</a> - Create and manage tournaments</li>
<li><a href="/admin/matches">Match Entry</a> - Record individual match results</li>
<li><a href="/admin/matches/upload">CSV Upload</a> - Bulk import matches from CSV</li>
</ul>
</div>
-76
View File
@@ -1,76 +0,0 @@
<h1>Edit Match #<%= match[:id] %></h1>
<div class="match-edit-form">
<form action="/admin/matches/<%= match[:id] %>" method="POST">
<input type="hidden" name="_method" value="PATCH">
<div class="form-section">
<h3>Team 1</h3>
<div class="form-group">
<label>Player 1</label>
<select name="match[team_1_p1_id]" required>
<option value="">Select player...</option>
<% all_players.each do |player| %>
<option value="<%= player[:id] %>" <%= 'selected' if player[:id] == match[:team_1_p1_id] %>>
<%= player[:name] %>
</option>
<% end %>
</select>
</div>
<div class="form-group">
<label>Player 2</label>
<select name="match[team_1_p2_id]" required>
<option value="">Select player...</option>
<% all_players.each do |player| %>
<option value="<%= player[:id] %>" <%= 'selected' if player[:id] == match[:team_1_p2_id] %>>
<%= player[:name] %>
</option>
<% end %>
</select>
</div>
<div class="form-group">
<label>Score</label>
<input type="number" name="match[team_1_score]" value="<%= match[:team_1_score] %>" min="0" max="10" required>
</div>
</div>
<div class="form-section">
<h3>Team 2</h3>
<div class="form-group">
<label>Player 1</label>
<select name="match[team_2_p1_id]" required>
<option value="">Select player...</option>
<% all_players.each do |player| %>
<option value="<%= player[:id] %>" <%= 'selected' if player[:id] == match[:team_2_p1_id] %>>
<%= player[:name] %>
</option>
<% end %>
</select>
</div>
<div class="form-group">
<label>Player 2</label>
<select name="match[team_2_p2_id]" required>
<option value="">Select player...</option>
<% all_players.each do |player| %>
<option value="<%= player[:id] %>" <%= 'selected' if player[:id] == match[:team_2_p2_id] %>>
<%= player[:name] %>
</option>
<% end %>
</select>
</div>
<div class="form-group">
<label>Score</label>
<input type="number" name="match[team_2_score]" value="<%= match[:team_2_score] %>" min="0" max="10" required>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Update Match</button>
<a href="/admin/matches" class="btn">Cancel</a>
</div>
</form>
</div>
<p class="help-text">
<strong>Note:</strong> Match scores can only be edited within 5 minutes of recording, or by a tournament/club administrator.
</p>
@@ -1,40 +0,0 @@
<h1>Match Administration</h1>
<p><a href="/admin/matches/new">Enter New Match</a> | <a href="/admin/matches/upload">Upload CSV</a></p>
<% if matches.any? %>
<table>
<thead>
<tr>
<th>ID</th>
<th>Date</th>
<th>Team 1</th>
<th>Score</th>
<th>Team 2</th>
<th>Score</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% matches.each do |match| %>
<tr>
<td data-label="ID"><%= match.id %></td>
<td data-label="Date"><%= match.played_at&.strftime('%Y-%m-%d %H:%M') %></td>
<td data-label="Team 1"><%= match.team_1_p1_name %> + <%= match.team_1_p2_name %></td>
<td data-label="Score 1"><strong><%= match.team_1_score %></strong></td>
<td data-label="Team 2"><%= match.team_2_p1_name %> + <%= match.team_2_p2_name %></td>
<td data-label="Score 2"><strong><%= match.team_2_score %></strong></td>
<td data-label="Status"><%= match.status %></td>
<td data-label="Actions">
<a href="/admin/matches/<%= match.id %>/edit">Edit</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No matches entered yet.</p>
<% end %>
<p><a href="/admin/players">Manage Players</a></p>
-106
View File
@@ -1,106 +0,0 @@
<h1><%= tournament ? "Enter Match for #{tournament.name}" : "Enter New Match" %></h1>
<% if tournament %>
<p><strong>Format:</strong> <%= tournament.format %> | <strong>Status:</strong> <%= tournament.status %></p>
<hr>
<% end %>
<form action="/admin/matches" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="event_id" value="<%= tournament&.id %>">
<div>
<label for="played_at">Date/Time:</label>
<input type="datetime-local" id="played_at" name="played_at" required value="<%= Time.now.strftime('%Y-%m-%dT%H:%M') %>">
</div>
<% if tournament && teams.any? %>
<!-- Team-based selection for tournaments -->
<fieldset>
<legend>Team 1</legend>
<div>
<label for="team_1_id">Select Team:</label>
<select id="team_1_id" name="team_1_id" required>
<option value="">Select team...</option>
<% teams.each do |team| %>
<option value="<%= team.id %>" <%= 'selected' if team.id == preselected_team_1 %>>
<%= team.team_name %>
</option>
<% end %>
</select>
</div>
</fieldset>
<fieldset>
<legend>Team 2</legend>
<div>
<label for="team_2_id">Select Team:</label>
<select id="team_2_id" name="team_2_id" required>
<option value="">Select team...</option>
<% teams.each do |team| %>
<option value="<%= team.id %>" <%= 'selected' if team.id == preselected_team_2 %>>
<%= team.team_name %>
</option>
<% end %>
</select>
</div>
</fieldset>
<% else %>
<!-- Individual player selection (non-tournament) -->
<fieldset>
<legend>Team 1</legend>
<div>
<label for="team_1_p1_id">Player 1:</label>
<select id="team_1_p1_id" name="team_1_p1_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_1_p2_id">Player 2:</label>
<select id="team_1_p2_id" name="team_1_p2_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_1_score">Score:</label>
<input type="number" id="team_1_score" name="team_1_score" min="0" max="10" required value="10">
</div>
</fieldset>
<fieldset>
<legend>Team 2</legend>
<div>
<label for="team_2_p1_id">Player 1:</label>
<select id="team_2_p1_id" name="team_2_p1_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_2_p2_id">Player 2:</label>
<select id="team_2_p2_id" name="team_2_p2_id" required>
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label for="team_2_score">Score:</label>
<input type="number" id="team_2_score" name="team_2_score" min="0" max="10" required value="7">
</div>
</fieldset>
<% end %>
<button type="submit">Save Match</button>
</form>
<p><a href="<%= tournament ? "/admin/tournaments/#{tournament.id}" : "/admin/matches" %>">Back</a></p>
-110
View File
@@ -1,110 +0,0 @@
<h1>Upload Tournament Results (CSV)</h1>
<p>Upload a CSV file with Euchre tournament results. The format supports standard Euchre scoring with Odds/Evens teams.</p>
<form action="/admin/matches/process_upload" method="post" enctype="multipart/form-data">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div class="form-group">
<label for="event_id">Tournament:</label>
<select id="event_id" name="event_id" required>
<option value="">Select a tournament...</option>
<% if defined?(tournaments) && tournaments %>
<% tournaments.each do |tournament| %>
<option value="<%= tournament.id %>"><%= tournament.name %></option>
<% end %>
<% end %>
</select>
</div>
<div class="form-group">
<label for="csv_file">CSV File:</label>
<input type="file" id="csv_file" name="csv_file" accept=".csv" required>
</div>
<button type="submit" class="btn-primary">Upload Results</button>
</form>
<p><a href="/admin/matches">Back to Matches</a></p>
<h2>Euchre Tournament CSV Format</h2>
<p>The CSV should have the following columns:</p>
<table>
<thead>
<tr>
<th>Column</th>
<th>Description</th>
<th>Example</th>
</tr>
</thead>
<tbody>
<tr>
<td>Event #</td>
<td>Tournament ID (can be omitted if selected above)</td>
<td>1</td>
</tr>
<tr>
<td>Round</td>
<td>Round number</td>
<td>1</td>
</tr>
<tr>
<td>Table</td>
<td>Table name (Clubs, Hearts, Diamonds, Spades, Stars)</td>
<td>Clubs</td>
</tr>
<tr>
<td>Seat 1</td>
<td>Player name (Odds team, position 1)</td>
<td>Derrick</td>
</tr>
<tr>
<td>Seat 3</td>
<td>Player name (Odds team, position 2)</td>
<td>Jesse C</td>
</tr>
<tr>
<td>Odds Points</td>
<td>Score for Odds team</td>
<td>5</td>
</tr>
<tr>
<td>Seat 2</td>
<td>Player name (Evens team, position 1)</td>
<td>Emma</td>
</tr>
<tr>
<td>Seat 4</td>
<td>Player name (Evens team, position 2)</td>
<td>Alissa</td>
</tr>
<tr>
<td>Evens Points</td>
<td>Score for Evens team</td>
<td>10</td>
</tr>
<tr>
<td>Winner</td>
<td>"Odds" or "Evens" (optional, for validation)</td>
<td>Evens</td>
</tr>
</tbody>
</table>
<h2>Example CSV:</h2>
<pre style="background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto;">
Event #,Round,Table,Seat 1,Seat 3,Odds Points,Seat 2,Seat 4,Evens Points,Winner
1,1,Clubs,Derrick,Jesse C,5,Emma,Alissa,10,Evens
1,1,Hearts,Kevin,Andy,8,Ellie,Jesse,6,Odds
1,1,Diamonds,Sara M,Amelia,10,Mike G,AJ,4,Odds
</pre>
<h3>Notes:</h3>
<ul>
<li>Player names must match exactly (including middle initials if present)</li>
<li>Table names are mapped to numeric IDs (Clubs=1, Hearts=2, etc.)</li>
<li>New players will be created automatically if they don't exist</li>
<li>Teams are automatically created for each player pair</li>
<li>Elo ratings are calculated after import</li>
</ul>
@@ -1 +0,0 @@
<h1>EuchreCamp::Views::Admin::Players::Destroy</h1>
@@ -1 +0,0 @@
<h1>EuchreCamp::Views::Admin::Players::Edit</h1>
@@ -1,29 +0,0 @@
<h1>EuchreCamp::Views::Admin::Players::Index</h1>
<h1>Player Administration</h1>
<a href="/admin/players/new">Create New Player</a>
<table>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Rating</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% players.each do |player| %>
<tr>
<td><%= player.id %></td>
<td><%= player.name %></td>
<td><%= player.rating %></td>
<td>
<a href="/admin/players/<%= player.id %>/edit">Edit</a>
<form action="/admin/players/<%= player.id %>" method="post" style="display:inline;">
<input type="hidden" name="_method" value="delete">
<button type="submit">Delete</button>
</form>
</td>
</tr>
<% end %>
</tbody>
</table>
-19
View File
@@ -1,19 +0,0 @@
<h1>Add New Player</h1>
<form action="/admin/players" method="POST" class="player-form">
<div class="form-group">
<label for="player_name">Player Name</label>
<input type="text" id="player_name" name="player[name]" required>
</div>
<div class="form-group">
<label for="player_rating">Initial Rating</label>
<input type="number" id="player_rating" name="player[rating]" value="1000" min="0" max="3000">
<small>Default rating is 1000</small>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary">Create Player</button>
<a href="/admin/players" class="btn">Cancel</a>
</div>
</form>
@@ -1,42 +0,0 @@
<h1>Edit Tournament: <%= tournament.name %></h1>
<form action="/admin/tournaments/<%= tournament.id %>" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="patch">
<div>
<label for="name">Tournament Name *</label>
<input type="text" id="name" name="name" value="<%= tournament.name %>" required>
</div>
<div>
<label for="description">Description</label>
<textarea id="description" name="description" rows="3"><%= tournament.description %></textarea>
</div>
<div>
<label for="event_date">Date/Time</label>
<input type="datetime-local" id="event_date" name="event_date"
value="<%= tournament.event_date&.strftime('%Y-%m-%dT%H:%M') %>">
</div>
<div>
<label for="format">Tournament Format</label>
<select id="format" name="format">
<option value="single_elim" <%= 'selected' if tournament.format == 'single_elim' %>>Single Elimination</option>
<option value="double_elim" <%= 'selected' if tournament.format == 'double_elim' %>>Double Elimination</option>
<option value="round_robin" <%= 'selected' if tournament.format == 'round_robin' %>>Round Robin</option>
<option value="swiss" <%= 'selected' if tournament.format == 'swiss' %>>Swiss System</option>
</select>
</div>
<div>
<label for="max_participants">Max Participants (teams)</label>
<input type="number" id="max_participants" name="max_participants"
min="2" max="32" value="<%= tournament.max_participants %>">
</div>
<button type="submit">Update Tournament</button>
</form>
<p><a href="/admin/tournaments/<%= tournament.id %>">Cancel</a></p>
@@ -1,64 +0,0 @@
<h1>Tournament Administration</h1>
<p><a href="/admin/tournaments/new">Create New Tournament</a></p>
<% if upcoming.any? %>
<h2>Upcoming / Active Tournaments</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Format</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% upcoming.each do |tournament| %>
<tr>
<td><%= tournament.name %></td>
<td><%= tournament.event_date&.strftime('%Y-%m-%d %H:%M') %></td>
<td><%= tournament.format %></td>
<td><%= tournament.status %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>">View</a> |
<a href="/admin/tournaments/<%= tournament.id %>/edit">Edit</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% if completed.any? %>
<h2>Past Tournaments</h2>
<table>
<thead>
<tr>
<th>Name</th>
<th>Date</th>
<th>Format</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% completed.each do |tournament| %>
<tr>
<td><%= tournament.name %></td>
<td><%= tournament.event_date&.strftime('%Y-%m-%d') %></td>
<td><%= tournament.format %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>">View Results</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% end %>
<% if upcoming.empty? && completed.empty? %>
<p>No tournaments yet. <a href="/admin/tournaments/new">Create your first tournament!</a></p>
<% end %>
<p><a href="/admin/players">Back to Admin</a></p>
@@ -1,40 +0,0 @@
<h1>Create New Tournament</h1>
<form action="/admin/tournaments" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div>
<label for="name">Tournament Name *</label>
<input type="text" id="name" name="name" required placeholder="e.g., Spring Championship">
</div>
<div>
<label for="description">Description</label>
<textarea id="description" name="description" rows="3" placeholder="Optional description or rules..."></textarea>
</div>
<div>
<label for="event_date">Date/Time</label>
<input type="datetime-local" id="event_date" name="event_date" value="<%= Time.now.strftime('%Y-%m-%dT%H:%M') %>">
</div>
<div>
<label for="format">Tournament Format</label>
<select id="format" name="format">
<option value="single_elim">Single Elimination</option>
<option value="double_elim">Double Elimination</option>
<option value="round_robin">Round Robin</option>
<option value="swiss">Swiss System</option>
</select>
</div>
<div>
<label for="max_participants">Max Participants (teams)</label>
<input type="number" id="max_participants" name="max_participants" min="2" max="32" placeholder="Optional">
<small>Leave blank for no limit</small>
</div>
<button type="submit">Create Tournament</button>
</form>
<p><a href="/admin/tournaments">Cancel</a></p>
@@ -1,44 +0,0 @@
<h1>Enter Results: <%= tournament.name %> - Round <%= round[:round_number] %></h1>
<form action="/admin/tournaments/<%= tournament.id %>/results" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<table>
<thead>
<tr>
<th>Matchup</th>
<th>Team 1</th>
<th>Score</th>
<th>vs</th>
<th>Team 2</th>
<th>Score</th>
</tr>
</thead>
<tbody>
<% matchups.each_with_index do |matchup, i| %>
<% team_1 = teams.find { |t| t.id == matchup[:team_1_id] } %>
<% team_2 = teams.find { |t| t.id == matchup[:team_2_id] } %>
<% next if team_1.nil? || team_2.nil? %>
<tr>
<td><%= i + 1 %></td>
<td><%= team_1.team_name %></td>
<td>
<input type="number" name="team_1_score_<%= matchup[:id] %>" min="0" max="10" value="10" style="width: 60px;">
</td>
<td>vs</td>
<td><%= team_2.team_name %></td>
<td>
<input type="number" name="team_2_score_<%= matchup[:id] %>" min="0" max="10" value="7" style="width: 60px;">
</td>
<td>
<input type="checkbox" name="matchup_<%= matchup[:id] %>" checked style="display:none;">
</td>
</tr>
<% end %>
</tbody>
</table>
<button type="submit">Save All Results</button>
</form>
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
@@ -1,47 +0,0 @@
<h1><%= tournament.name %> - Round <%= round[:round_number] %></h1>
<p><strong>Status:</strong> <%= round[:status] %></p>
<h2>Matchups</h2>
<% if matchups.any? %>
<table>
<thead>
<tr>
<th>Position</th>
<th>Team 1</th>
<th>vs</th>
<th>Team 2</th>
<th>Winner</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% matchups.each do |matchup| %>
<% team_1 = teams.find { |t| t.id == matchup[:team_1_id] } %>
<% team_2 = teams.find { |t| t.id == matchup[:team_2_id] } %>
<% winner = teams.find { |t| t.id == matchup[:winner_team_id] } %>
<tr>
<td><%= matchup[:bracket_position] %></td>
<td><%= team_1&.team_name || 'BYE' %></td>
<td>vs</td>
<td><%= team_2&.team_name || 'BYE' %></td>
<td><%= winner&.team_name || '-' %></td>
<td>
<% if matchup[:match_id].nil? %>
<a href="/admin/matches/new?event_id=<%= tournament.id %>&team_1_id=<%= matchup[:team_1_id] %>&team_2_id=<%= matchup[:team_2_id] %>">
Create Match
</a>
<% else %>
<a href="/admin/matches/<%= matchup[:match_id] %>">View Match</a>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No matchups scheduled for this round.</p>
<% end %>
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
@@ -1,236 +0,0 @@
<h1><%= tournament.name %></h1>
<% if tournament.description %>
<p><em><%= tournament.description %></em></p>
<% end %>
<div class="tournament-meta">
<strong>Format:</strong> <%= tournament.format %> |
<strong>Status:</strong> <%= tournament.status %> |
<% if tournament.event_date %>
<strong>Date:</strong> <%= tournament.event_date.strftime('%Y-%m-%d %H:%M') %>
<% end %>
</div>
<hr>
<div class="tournament-tabs">
<ul>
<li><a href="#participants">Participants</a></li>
<li><a href="#teams">Teams</a></li>
<li><a href="#rounds">Rounds & Schedule</a></li>
<li><a href="#standings">Standings</a></li>
</ul>
</div>
<!-- Participants Section -->
<section id="participants">
<h2>Participants</h2>
<% if players.any? %>
<p><strong>Total:</strong> <%= players.count %> players registered</p>
<table>
<thead>
<tr>
<th>Player</th>
<th>Status</th>
<th>Seed</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% players.each do |player| %>
<% participant = participants.find { |p| p.player_id == player.id } %>
<tr>
<td><%= player.name %></td>
<td><%= participant&.status || 'registered' %></td>
<td><%= participant&.seed || '-' %></td>
<td>
<form action="/admin/tournaments/<%= tournament.id %>/participants/<%= player.id %>" method="post" style="display:inline;">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="delete">
<button type="submit" onclick="return confirm('Remove <%= player.name %> from tournament?')" style="background:none;border:none;color:blue;text-decoration:underline;cursor:pointer;">Remove</button>
</form>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No participants registered yet.</p>
<% end %>
<h3>Add Participants</h3>
<% if available_players.any? %>
<form action="/admin/tournaments/<%= tournament.id %>/participants" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<label>Add Player:</label>
<select name="player_id">
<option value="">Select a player...</option>
<% available_players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
<button type="submit">Add to Tournament</button>
</form>
<% else %>
<p><em>All available players are already registered.</em></p>
<% end %>
</section>
<!-- Teams Section -->
<section id="teams">
<h2>Teams</h2>
<% if teams.any? %>
<p><strong>Total:</strong> <%= teams.count %> teams</p>
<table>
<thead>
<tr>
<th>Team Name</th>
<th>Player 1</th>
<th>Player 2</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% teams.each do |team| %>
<% p1 = players.find { |p| p.id == team.player_1_id } %>
<% p2 = players.find { |p| p.id == team.player_2_id } %>
<tr>
<td><%= team.team_name %></td>
<td><%= p1&.name || team.player_1_id %></td>
<td><%= p2&.name || team.player_2_id %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>/edit">Edit</a> |
<form action="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>" method="post" style="display:inline;">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="delete">
<button type="submit" onclick="return confirm('Remove team?')" style="background:none;border:none;color:blue;text-decoration:underline;cursor:pointer;">Remove</button>
</form>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No teams created yet.</p>
<% end %>
<h3>Create Teams</h3>
<% if players.any? %>
<form action="/admin/tournaments/<%= tournament.id %>/teams" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div>
<label>Team Name:</label>
<input type="text" name="team_name" placeholder="e.g., Ace High">
</div>
<div>
<label>Player 1:</label>
<select name="player_1_id">
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<div>
<label>Player 2:</label>
<select name="player_2_id">
<option value="">Select player...</option>
<% players.each do |player| %>
<option value="<%= player.id %>"><%= player.name %></option>
<% end %>
</select>
</div>
<button type="submit">Create Team</button>
</form>
<% else %>
<p><em>Add participants first before creating teams.</em></p>
<% end %>
</section>
<!-- Rounds Section -->
<section id="rounds">
<h2>Rounds & Schedule</h2>
<% if rounds.any? %>
<table>
<thead>
<tr>
<th>Round</th>
<th>Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% rounds.each do |round| %>
<tr>
<td>Round <%= round.round_number %></td>
<td><%= round.status %></td>
<td>
<a href="/admin/tournaments/<%= tournament.id %>/rounds/<%= round.id %>">View</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No rounds scheduled yet.</p>
<% end %>
<% if teams.any? %>
<form action="/admin/tournaments/<%= tournament.id %>/schedule" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<button type="submit">Generate Schedule / Rounds</button>
</form>
<% else %>
<p><em>Add teams first to generate the schedule.</em></p>
<% end %>
</section>
<!-- Standings Section -->
<section id="standings">
<h2>Standings</h2>
<% if teams.any? %>
<table>
<thead>
<tr>
<th>#</th>
<th>Team</th>
<th>W</th>
<th>L</th>
<th>PCT</th>
<th>PF</th>
<th>PA</th>
<th>PD</th>
</tr>
</thead>
<tbody>
<% teams.each_with_index do |team, i| %>
<tr>
<td><%= i + 1 %></td>
<td><%= team.team_name %></td>
<td>0</td>
<td>0</td>
<td>.000</td>
<td>0</td>
<td>0</td>
<td>0</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No teams to display standings.</p>
<% end %>
</section>
<hr>
<div class="tournament-actions">
<a href="/admin/tournaments/<%= tournament.id %>/results">Enter Match Results</a> |
<a href="/admin/tournaments/<%= tournament.id %>/complete">Complete Tournament</a> |
<a href="/admin/tournaments">Back to Tournaments</a>
</div>
@@ -1,39 +0,0 @@
<h1>Edit Team: <%= team.team_name %></h1>
<p><strong>Tournament:</strong> <%= tournament.name %></p>
<form action="/admin/tournaments/<%= tournament.id %>/teams/<%= team.id %>" method="post">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<input type="hidden" name="_method" value="patch">
<div>
<label for="team_name">Team Name</label>
<input type="text" id="team_name" name="team_name" value="<%= team.team_name %>" required>
</div>
<div>
<label for="player_1_id">Player 1</label>
<select id="player_1_id" name="player_1_id" required>
<% players.each do |player| %>
<option value="<%= player.id %>" <%= 'selected' if player.id == team.player_1_id %>>
<%= player.name %>
</option>
<% end %>
</select>
</div>
<div>
<label for="player_2_id">Player 2</label>
<select id="player_2_id" name="player_2_id" required>
<% players.each do |player| %>
<option value="<%= player.id %>" <%= 'selected' if player.id == team.player_2_id %>>
<%= player.name %>
</option>
<% end %>
</select>
</div>
<button type="submit">Update Team</button>
</form>
<p><a href="/admin/tournaments/<%= tournament.id %>">Back to Tournament</a></p>
-40
View File
@@ -1,40 +0,0 @@
<div class="auth-container">
<div class="auth-card">
<h1>Login</h1>
<% if flash[:error] %>
<div class="alert alert-error"><%= flash[:error] %></div>
<% end %>
<% if flash[:success] %>
<div class="alert alert-success"><%= flash[:success] %></div>
<% end %>
<form action="/auth/login" method="POST">
<input type="hidden" name="_csrf_token" value="<%= csrf_token %>">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password">
</div>
<div class="form-group remember-me">
<label>
<input type="checkbox" name="remember" value="1">
Remember me
</label>
</div>
<button type="submit" class="btn btn-primary btn-block">Login</button>
</form>
<div class="auth-links">
<p>Don't have an account? <a href="/register">Register here</a></p>
<p><a href="/password/reset">Forgot password?</a></p>
</div>
</div>
</div>
-106
View File
@@ -1,106 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="theme-color" content="#2d5a27">
<meta name="description" content="EuchreCamp - Track your Euchre games and tournaments">
<title>EuchreCamp</title>
<%= favicon_tag %>
<%= stylesheet_tag "app" %>
<!-- PWA Manifest -->
<link rel="manifest" href="/manifest.json">
</head>
<body>
<!-- Desktop Navigation -->
<nav class="main-nav">
<div class="nav-brand">
<a href="/">EuchreCamp</a>
</div>
<div class="nav-links">
<a href="/rankings">Rankings</a>
<% if defined?(current_player) && current_player %>
<a href="/players/<%= current_player.id %>/profile">My Profile</a>
<a href="/players/<%= current_player.id %>/schedule">My Schedule</a>
<% if current_player.respond_to?(:admin?) && current_player.admin? %>
<a href="/admin">Admin</a>
<% end %>
<% end %>
</div>
<div class="nav-user">
<% if defined?(current_player) && current_player %>
<span><%= current_player.name %></span>
<a href="/logout">Logout</a>
<% else %>
<a href="/login">Login</a>
<a href="/register">Register</a>
<% end %>
</div>
</nav>
<!-- Main Content -->
<main class="main-content">
<%= yield %>
</main>
<!-- Bottom Navigation (Mobile) -->
<nav class="bottom-nav">
<a href="/rankings" class="bottom-nav-item" id="nav-rankings">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span>Rankings</span>
</a>
<% if defined?(current_player) && current_player %>
<a href="/players/<%= current_player.id %>/schedule" class="bottom-nav-item" id="nav-schedule">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span>Schedule</span>
</a>
<a href="/players/<%= current_player.id %>/profile" class="bottom-nav-item" id="nav-profile">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
</svg>
<span>Profile</span>
</a>
<% if current_player.respond_to?(:admin?) && current_player.admin? %>
<a href="/admin" class="bottom-nav-item" id="nav-admin">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
<span>Admin</span>
</a>
<% end %>
<% else %>
<a href="/login" class="bottom-nav-item" id="nav-login">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
</svg>
<span>Login</span>
</a>
<% end %>
</nav>
<%= javascript_tag "app" %>
<!-- Service Worker Registration -->
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js')
.then(registration => {
console.log('ServiceWorker registration successful');
})
.catch(error => {
console.log('ServiceWorker registration failed:', error);
});
});
}
</script>
</body>
</html>
-94
View File
@@ -1,94 +0,0 @@
<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>
-32
View File
@@ -1,32 +0,0 @@
<h1>Player Rankings</h1>
<% if players.any? %>
<table>
<thead>
<tr>
<th>Rank</th>
<th>Player</th>
<th>Elo Rating</th>
<th>Original Rating</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% players.each_with_index do |player, index| %>
<tr>
<td><%= index + 1 %></td>
<td><%= player.name %></td>
<td><strong><%= player.current_elo %></strong></td>
<td><%= player.rating %></td>
<td>
<a href="/players/<%= player.id %>/profile">Profile</a>
</td>
</tr>
<% end %>
</tbody>
</table>
<% else %>
<p>No players found.</p>
<% end %>
<p><a href="/admin/players">Admin</a></p>
-90
View File
@@ -1,90 +0,0 @@
<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>
-5
View File
@@ -1,5 +0,0 @@
<h1>EuchreCamp::Views::Players::Show</h1>
<h2><%= player.name %></h2>
<h3><%= player.rating %></h3>
-9
View File
@@ -1,9 +0,0 @@
# auto_register: false
# frozen_string_literal: true
require "hanami/view"
module EuchreCamp
class View < Hanami::View
end
end
-14
View File
@@ -1,14 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
class Index < EuchreCamp::View
expose :total_players
expose :active_tournaments
expose :completed_tournaments
expose :recent_matches
end
end
end
end
-14
View File
@@ -1,14 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class Edit < EuchreCamp::View
expose :match
expose :all_players
end
end
end
end
end
-13
View File
@@ -1,13 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class Index < EuchreCamp::View
expose :matches
end
end
end
end
end
-17
View File
@@ -1,17 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class New < EuchreCamp::View
expose :players
expose :tournament
expose :teams
expose :preselected_team_1
expose :preselected_team_2
end
end
end
end
end
-13
View File
@@ -1,13 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class Update < EuchreCamp::View
# No template needed - this action redirects
end
end
end
end
end
-12
View File
@@ -1,12 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Matches
class Upload < EuchreCamp::View
end
end
end
end
end
-12
View File
@@ -1,12 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Players
class Destroy < EuchreCamp::View
end
end
end
end
end
-12
View File
@@ -1,12 +0,0 @@
# frozen_string_literal: true
module EuchreCamp
module Views
module Admin
module Players
class Edit < EuchreCamp::View
end
end
end
end
end

Some files were not shown because too many files have changed in this diff Show More