feat: initialize Next.js project with Prisma, authentication, and rankings page
This commit is contained in:
+43
-8
@@ -1,8 +1,43 @@
|
|||||||
.env
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
log/*
|
|
||||||
public/assets/
|
# dependencies
|
||||||
node_modules/
|
/node_modules
|
||||||
spec/examples.txt
|
/.pnp
|
||||||
Gemfile.lock
|
.pnp.*
|
||||||
cookies.txt
|
.yarn/*
|
||||||
euchre_camp.db
|
!.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 +0,0 @@
|
|||||||
ruby 3.3.3
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
web: bundle exec hanami server
|
|
||||||
assets: bundle exec hanami assets watch
|
|
||||||
@@ -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
|
First, run the development server:
|
||||||
|
|
||||||
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:**
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
npm run dev
|
||||||
cd euchre_camp
|
# 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
|
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||||
bundle install
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
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
|
## Learn More
|
||||||
bundle exec rake db:migrate
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Compile assets:**
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
```bash
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
./node_modules/.bin/esbuild app/assets/css/app.css --bundle --outfile=public/assets/app-GVDAEYEC.css
|
- [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
|
## Deploy on Vercel
|
||||||
bundle exec puma -p 3000 -e development
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
- **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)
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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 |
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
require 'rom-repository'
|
|
||||||
|
|
||||||
module EuchreCamp
|
|
||||||
class Repository < ROM::Repository::Root
|
|
||||||
include Deps[container: 'persistence.rom']
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<h1>EuchreCamp::Views::Players::Show</h1>
|
|
||||||
|
|
||||||
<h2><%= player.name %></h2>
|
|
||||||
|
|
||||||
<h3><%= player.rating %></h3>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# auto_register: false
|
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require "hanami/view"
|
|
||||||
|
|
||||||
module EuchreCamp
|
|
||||||
class View < Hanami::View
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
Reference in New Issue
Block a user