Switch to user_id auth and update docs
This commit is contained in:
235
README.md
235
README.md
@@ -4,30 +4,49 @@ A lightweight, responsive web application for tracking Magic: The Gathering EDH/
|
||||
|
||||
## Features
|
||||
|
||||
- **Secure Authentication**: JWT-based login system with password hashing
|
||||
- **Commander Management**: Track all your commanders with MTG color identity
|
||||
- **Game Logging**: Detailed game statistics including:
|
||||
- Player count (2-8 players)
|
||||
- Commander played
|
||||
- Game date and duration
|
||||
- Win/loss tracking
|
||||
- Round count
|
||||
- Starting player advantage
|
||||
- Sol Ring turn one impact
|
||||
- **Statistics Dashboard**: Visual analytics with Chart.js
|
||||
- **Live Round Counter**: Track ongoing games in real-time
|
||||
- **Responsive Design**: Mobile-friendly interface with Tailwind CSS
|
||||
### ✅ Implemented
|
||||
- **Secure Authentication**: JWT-based login/registration system with password hashing (HS512).
|
||||
- **Commander Management**:
|
||||
- CRUD operations for Commanders.
|
||||
- MTG Color Identity picker (WUBRG).
|
||||
- Search and filter functionality.
|
||||
- Validation for names and colors.
|
||||
- **Game Logging**:
|
||||
- Log game results (Win/Loss).
|
||||
- Track player count, rounds, and specific win conditions (Starting Player, Sol Ring T1).
|
||||
- Associate games with specific Commanders.
|
||||
- **Statistics Dashboard**:
|
||||
- **Overview**: Total games, win rate, active decks, average rounds.
|
||||
- **Visualizations**:
|
||||
- Win Rate by Color Identity (Chart.js Doughnut).
|
||||
- Win Rate by Player Count (Chart.js Bar).
|
||||
- **Detailed Tables**: Per-commander performance metrics.
|
||||
- **Responsive UI**: Mobile-friendly design using Tailwind CSS and Alpine.js.
|
||||
- **Infrastructure**: Docker Compose setup for development and production.
|
||||
|
||||
### 🚧 Pending / Roadmap
|
||||
- **Live Round Counter**: Interactive real-time counter during games (currently post-game entry only).
|
||||
- **Advanced Trends**: Historical performance trends over time (endpoints `/api/stats/trends` not yet implemented).
|
||||
- **Commander Comparison**: Direct head-to-head comparison tool.
|
||||
- **HTTPS Configuration**: Production Nginx setup with SSL.
|
||||
- **Unit/Integration Tests**: Comprehensive test suite (currently partial).
|
||||
|
||||
## Technology Stack
|
||||
|
||||
- **Backend**: Fastify with JWT authentication (HS512)
|
||||
- **Database**: SQLite with volume persistence
|
||||
- **Frontend**: Alpine.js (~10KB) with Tailwind CSS
|
||||
- **Deployment**: Docker Compose with multi-stage builds
|
||||
- **Charts**: Chart.js with Alpine.js reactivity
|
||||
- **Backend**: Fastify (Node.js v20+)
|
||||
- **Database**: SQLite (better-sqlite3) with WAL mode
|
||||
- **Frontend**: Alpine.js, Tailwind CSS (CDN)
|
||||
- **Visualization**: Chart.js
|
||||
- **Containerization**: Docker & Docker Compose
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
- Docker & Docker Compose
|
||||
- Git
|
||||
|
||||
### Running with Docker (Recommended)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repository-url>
|
||||
@@ -37,149 +56,85 @@ cd edh-stats
|
||||
docker-compose up -d
|
||||
|
||||
# Access the application
|
||||
# Frontend: http://localhost:80
|
||||
# Backend API: http://localhost:3000
|
||||
# Frontend: http://localhost:8081
|
||||
# Backend API: http://localhost:3002
|
||||
```
|
||||
|
||||
> **Note:** Default ports are `8081` (Frontend) and `3002` (Backend) to avoid conflicts.
|
||||
|
||||
### Local Development
|
||||
|
||||
If you prefer running without Docker:
|
||||
|
||||
```bash
|
||||
# Backend
|
||||
cd backend
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# Frontend (served via simple HTTP server or Nginx)
|
||||
cd frontend
|
||||
# Use any static file server, e.g., 'serve'
|
||||
npx serve public -p 8081
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
edh-stats/
|
||||
├── README.md # This documentation
|
||||
├── docker-compose.yml # Development environment
|
||||
├── docker-compose.prod.yml # Production environment
|
||||
├── .env.example # Environment variables template
|
||||
├── .gitignore # Git ignore patterns
|
||||
├── backend/
|
||||
│ ├── Dockerfile # Multi-stage Docker build
|
||||
│ ├── package.json # Node.js dependencies
|
||||
│ ├── .dockerignore # Docker build optimizations
|
||||
│ └── src/
|
||||
│ ├── server.js # Main application entry point
|
||||
│ ├── config/
|
||||
│ ├── models/
|
||||
│ ├── routes/
|
||||
│ ├── middleware/
|
||||
│ ├── database/
|
||||
│ └── utils/
|
||||
│ ├── src/
|
||||
│ │ ├── config/ # DB & Auth config
|
||||
│ │ ├── database/ # Migrations & Seeds
|
||||
│ │ ├── models/ # Data access layer (Commander, Game, User)
|
||||
│ │ ├── routes/ # API endpoints
|
||||
│ │ └── server.js # App entry point
|
||||
│ └── Dockerfile
|
||||
├── frontend/
|
||||
│ ├── public/ # HTML pages
|
||||
│ ├── css/ # Compiled styles
|
||||
│ └── js/ # Alpine.js components
|
||||
└── database/
|
||||
└── data/ # SQLite data directory
|
||||
│ ├── public/ # Static assets
|
||||
│ │ ├── css/ # Custom styles
|
||||
│ │ ├── js/ # Alpine.js logic
|
||||
│ │ └── *.html # Views
|
||||
│ └── Dockerfile
|
||||
├── database/ # Persisted SQLite data
|
||||
├── docker-compose.yml # Dev orchestration
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication (`/api/auth`)
|
||||
- `POST /register` - User registration
|
||||
- `POST /login` - JWT token generation
|
||||
- `POST /refresh` - Token refresh
|
||||
- `POST /register`
|
||||
- `POST /login`
|
||||
- `GET /me`
|
||||
|
||||
### Commanders (`/api/commanders`)
|
||||
- `GET /` - List user's commanders
|
||||
- `POST /` - Create new commander
|
||||
- `PUT /:id` - Update commander
|
||||
- `DELETE /:id` - Delete commander
|
||||
- `GET /` - List/Search
|
||||
- `POST /` - Create
|
||||
- `GET /popular` - Top commanders
|
||||
- `GET /:id` - Details
|
||||
- `PUT /:id` - Update
|
||||
- `DELETE /:id` - Remove
|
||||
|
||||
### Games (`/api/games`)
|
||||
- `GET /` - List games with filtering
|
||||
- `POST /` - Log new game
|
||||
- `PUT /:id` - Update game
|
||||
- `DELETE /:id` - Delete game
|
||||
- `GET /` - History
|
||||
- `POST /` - Log result
|
||||
- `PUT /:id` - Edit record
|
||||
- `DELETE /:id` - Remove record
|
||||
|
||||
### Statistics (`/api/stats`)
|
||||
- `GET /overview` - Overall statistics
|
||||
- `GET /commanders/:id` - Commander performance
|
||||
- `GET /trends` - Performance trends
|
||||
- `GET /comparison` - Commander comparison
|
||||
- `GET /overview` - KPI cards
|
||||
- `GET /commanders` - Detailed breakdown & charts
|
||||
|
||||
## Database Schema
|
||||
## Development Notes
|
||||
|
||||
The application uses SQLite with the following main tables:
|
||||
### Database
|
||||
The SQLite database file is stored in `./database/data/edh-stats.db`. It uses `PRAGMA journal_mode = WAL` for performance.
|
||||
Migrations are run automatically on server start if `NODE_ENV != 'test'`.
|
||||
|
||||
- `users` - User accounts and authentication
|
||||
- `commanders` - Commander cards with color identity
|
||||
- `games` - Game records with detailed statistics
|
||||
|
||||
## Development
|
||||
|
||||
### Prerequisites
|
||||
- Docker and Docker Compose
|
||||
- Node.js 20+ (for local development)
|
||||
- Git
|
||||
|
||||
### Development Setup
|
||||
|
||||
```bash
|
||||
# Install dependencies (backend)
|
||||
cd backend
|
||||
npm install
|
||||
|
||||
# Install dependencies (frontend)
|
||||
cd ../frontend
|
||||
npm install
|
||||
|
||||
# Start development environment
|
||||
docker-compose up
|
||||
|
||||
# Or run locally
|
||||
cd backend && npm run dev
|
||||
cd frontend && npm run dev
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file from `.env.example`:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Configure the following variables:
|
||||
- `JWT_SECRET` - JWT signing secret
|
||||
- `DATABASE_PATH` - SQLite database file location
|
||||
- `NODE_ENV` - Environment (development/production)
|
||||
- `CORS_ORIGIN` - Frontend domain
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production Deployment
|
||||
|
||||
```bash
|
||||
# Build and deploy to production
|
||||
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop application
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### Security Notes
|
||||
|
||||
- Use strong JWT secrets in production
|
||||
- Enable HTTPS in production
|
||||
- Regular database backups recommended
|
||||
- Monitor resource usage and logs
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Add tests if applicable
|
||||
5. Submit a pull request
|
||||
### Frontend State
|
||||
State management is handled by Alpine.js components (`commanderManager`, `gameManager`, `statsManager`).
|
||||
Authentication tokens are stored in `localStorage` or `sessionStorage`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
||||
## Support
|
||||
|
||||
For issues and questions:
|
||||
- Create an issue in the repository
|
||||
- Check the documentation in `/docs`
|
||||
- Review API documentation at `/docs/API.md`
|
||||
MIT
|
||||
|
||||
@@ -28,8 +28,8 @@ class Commander {
|
||||
const commander = db.prepare(`
|
||||
SELECT id, name, colors, user_id, created_at, updated_at
|
||||
FROM commanders
|
||||
WHERE id = ? AND dbManager.getCurrentUser() = ?
|
||||
`).get([id, dbManager.getCurrentUser()])
|
||||
WHERE id = ?
|
||||
`).get([id])
|
||||
|
||||
return commander
|
||||
} catch (error) {
|
||||
@@ -44,7 +44,7 @@ class Commander {
|
||||
const query = `
|
||||
SELECT id, name, colors, user_id, created_at, updated_at
|
||||
FROM commanders
|
||||
WHERE dbManager.getCurrentUser() = ?
|
||||
WHERE user_id = ?
|
||||
`
|
||||
|
||||
if (sortBy) {
|
||||
@@ -110,7 +110,7 @@ class Commander {
|
||||
const result = db.prepare(`
|
||||
UPDATE commanders
|
||||
SET ${updates.join(', ')}
|
||||
WHERE id = ? AND dbManager.getCurrentUser() = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
`).run([...values, id, userId])
|
||||
|
||||
return result.changes > 0
|
||||
@@ -131,7 +131,7 @@ class Commander {
|
||||
|
||||
const result = db.prepare(`
|
||||
DELETE FROM commanders
|
||||
WHERE id = ? AND dbManager.getCurrentUser() = ?
|
||||
WHERE id = ? AND user_id = ?
|
||||
`).run([id, userId])
|
||||
|
||||
return result.changes > 0
|
||||
@@ -184,7 +184,7 @@ class Commander {
|
||||
const commanders = db.prepare(`
|
||||
SELECT id, name, colors, user_id, created_at, updated_at
|
||||
FROM commanders
|
||||
WHERE dbManager.getCurrentUser() = ?
|
||||
WHERE user_id = ?
|
||||
ORDER BY name ASC
|
||||
LIMIT ?
|
||||
`).all([userId, searchQuery, limit])
|
||||
|
||||
@@ -25,7 +25,6 @@ export default async function commanderRoutes(fastify, options) {
|
||||
preHandler: [async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
dbManager.setCurrentUser(request.user.id)
|
||||
} catch (err) {
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
@@ -63,7 +62,6 @@ export default async function commanderRoutes(fastify, options) {
|
||||
preHandler: [async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
dbManager.setCurrentUser(request.user.id)
|
||||
} catch (err) {
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
@@ -107,7 +105,6 @@ export default async function commanderRoutes(fastify, options) {
|
||||
preHandler: [async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
dbManager.setCurrentUser(request.user.id)
|
||||
} catch (err) {
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
@@ -117,8 +114,9 @@ export default async function commanderRoutes(fastify, options) {
|
||||
}]
|
||||
}, async (request, reply) => {
|
||||
try {
|
||||
const validatedData = createCommanderSchema.parse(request.body)
|
||||
// Manually parse since fastify.decorate request.user is set by jwtVerify
|
||||
const userId = request.user.id
|
||||
const validatedData = createCommanderSchema.parse(request.body)
|
||||
|
||||
const commander = await Commander.create({
|
||||
...validatedData,
|
||||
@@ -155,7 +153,6 @@ export default async function commanderRoutes(fastify, options) {
|
||||
preHandler: [async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
dbManager.setCurrentUser(request.user.id)
|
||||
} catch (err) {
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
@@ -211,7 +208,6 @@ export default async function commanderRoutes(fastify, options) {
|
||||
preHandler: [async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
dbManager.setCurrentUser(request.user.id)
|
||||
} catch (err) {
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
@@ -251,7 +247,6 @@ export default async function commanderRoutes(fastify, options) {
|
||||
preHandler: [async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
dbManager.setCurrentUser(request.user.id)
|
||||
} catch (err) {
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
@@ -287,7 +282,6 @@ export default async function commanderRoutes(fastify, options) {
|
||||
preHandler: [async (request, reply) => {
|
||||
try {
|
||||
await request.jwtVerify()
|
||||
dbManager.setCurrentUser(request.user.id)
|
||||
} catch (err) {
|
||||
reply.code(401).send({
|
||||
error: 'Unauthorized',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Commander management Alpine.js components
|
||||
function commanderManager() {
|
||||
return {
|
||||
// State
|
||||
showAddForm: false,
|
||||
editingCommander: null,
|
||||
commanders: [],
|
||||
@@ -10,15 +11,17 @@ function commanderManager() {
|
||||
searchQuery: '',
|
||||
submitting: false,
|
||||
editSubmitting: false,
|
||||
serverError: '',
|
||||
|
||||
// Form Data
|
||||
newCommander: {
|
||||
name: '',
|
||||
colors: []
|
||||
},
|
||||
errors: {},
|
||||
editErrors: {},
|
||||
serverError: '',
|
||||
|
||||
// MTG Color data
|
||||
// Constants
|
||||
mtgColors: [
|
||||
{ id: 'W', name: 'White', hex: '#F0E6D2' },
|
||||
{ id: 'U', name: 'Blue', hex: '#0E68AB' },
|
||||
@@ -27,17 +30,18 @@ function commanderManager() {
|
||||
{ id: 'G', name: 'Green', hex: '#5A7A3B' }
|
||||
],
|
||||
|
||||
// Lifecycle
|
||||
async init() {
|
||||
await this.loadCommanders()
|
||||
},
|
||||
|
||||
// API Methods
|
||||
async loadCommanders() {
|
||||
this.loading = true
|
||||
try {
|
||||
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
|
||||
const response = await fetch('/api/commanders', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
||||
}
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
@@ -58,10 +62,9 @@ function commanderManager() {
|
||||
this.loading = true
|
||||
this.showPopular = true
|
||||
try {
|
||||
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
|
||||
const response = await fetch('/api/commanders/popular', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
||||
}
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
@@ -71,13 +74,14 @@ function commanderManager() {
|
||||
this.serverError = 'Failed to load popular commanders'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Load popular commanders error:', error)
|
||||
console.error('Load popular error:', error)
|
||||
this.serverError = 'Network error occurred'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
// Validation
|
||||
validateCommanderName() {
|
||||
if (!this.newCommander.name.trim()) {
|
||||
this.errors.name = 'Commander name is required'
|
||||
@@ -91,6 +95,7 @@ function commanderManager() {
|
||||
},
|
||||
|
||||
validateEditCommanderName() {
|
||||
if (!this.editingCommander) return
|
||||
if (!this.editingCommander.name.trim()) {
|
||||
this.editErrors.name = 'Commander name is required'
|
||||
} else if (this.editingCommander.name.length < 2) {
|
||||
@@ -102,21 +107,20 @@ function commanderManager() {
|
||||
}
|
||||
},
|
||||
|
||||
// Actions
|
||||
async handleAddCommander() {
|
||||
this.validateCommanderName()
|
||||
|
||||
if (this.errors.name) {
|
||||
return
|
||||
}
|
||||
if (this.errors.name) return
|
||||
|
||||
this.submitting = true
|
||||
this.serverError = ''
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
|
||||
const response = await fetch('/api/commanders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.newCommander)
|
||||
@@ -126,7 +130,6 @@ function commanderManager() {
|
||||
const data = await response.json()
|
||||
this.commanders.unshift(data.commander)
|
||||
this.resetAddForm()
|
||||
await this.loadCommanders() // Refresh the list
|
||||
} else {
|
||||
const errorData = await response.json()
|
||||
this.serverError = errorData.message || 'Failed to create commander'
|
||||
@@ -140,20 +143,20 @@ function commanderManager() {
|
||||
},
|
||||
|
||||
async handleUpdateCommander() {
|
||||
if (!this.editingCommander) return
|
||||
|
||||
this.validateEditCommanderName()
|
||||
|
||||
if (this.editErrors.name) {
|
||||
return
|
||||
}
|
||||
if (this.editErrors.name) return
|
||||
|
||||
this.editSubmitting = true
|
||||
this.serverError = ''
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
|
||||
const response = await fetch(`/api/commanders/${this.editingCommander.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`,
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
@@ -164,7 +167,6 @@ function commanderManager() {
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
// Update the commander in the list
|
||||
const index = this.commanders.findIndex(c => c.id === this.editingCommander.id)
|
||||
if (index !== -1) {
|
||||
this.commanders[index] = data.commander
|
||||
@@ -181,6 +183,62 @@ function commanderManager() {
|
||||
}
|
||||
},
|
||||
|
||||
async deleteCommander(commander) {
|
||||
if (!confirm(`Are you sure you want to delete "${commander.name}"?`)) return
|
||||
|
||||
try {
|
||||
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
|
||||
const response = await fetch(`/api/commanders/${commander.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
this.commanders = this.commanders.filter(c => c.id !== commander.id)
|
||||
} else {
|
||||
this.serverError = 'Failed to delete commander'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete error:', error)
|
||||
this.serverError = 'Network error occurred'
|
||||
}
|
||||
},
|
||||
|
||||
// Search
|
||||
async searchCommanders() {
|
||||
if (!this.searchQuery.trim()) {
|
||||
await this.loadCommanders()
|
||||
return
|
||||
}
|
||||
this.loading = true
|
||||
try {
|
||||
const token = localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')
|
||||
const response = await fetch(`/api/commanders?q=${encodeURIComponent(this.searchQuery)}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` }
|
||||
})
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
this.commanders = data.commanders || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search error:', error)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
debounceSearch() {
|
||||
clearTimeout(this._searchTimeout)
|
||||
this._searchTimeout = setTimeout(() => {
|
||||
if (this.showPopular) {
|
||||
this.loadCommanders() // Reset to normal view if searching
|
||||
this.showPopular = false
|
||||
}
|
||||
this.searchCommanders()
|
||||
}, 300)
|
||||
},
|
||||
|
||||
// UI Helpers
|
||||
toggleNewColor(colorId) {
|
||||
const index = this.newCommander.colors.indexOf(colorId)
|
||||
if (index > -1) {
|
||||
@@ -191,10 +249,11 @@ function commanderManager() {
|
||||
},
|
||||
|
||||
toggleEditColor(colorId) {
|
||||
if (!this.editingCommander) return
|
||||
if (!this.editingCommander.colors) this.editingCommander.colors = []
|
||||
const index = this.editingCommander.colors.indexOf(colorId)
|
||||
if (index > -1) {
|
||||
this.editingCommander.colors.splice(index, 1)
|
||||
this.editingCommander.colors = this.editingCommander.colors.filter(c => c !== colorId)
|
||||
} else {
|
||||
this.editingCommander.colors.push(colorId)
|
||||
}
|
||||
@@ -214,33 +273,12 @@ function commanderManager() {
|
||||
: 'ring-1 ring-offset-1 border-gray-300 hover:border-gray-400'
|
||||
},
|
||||
|
||||
async deleteCommander(commander) {
|
||||
if (!confirm(`Are you sure you want to delete "${commander.name}"? This action cannot be undone.`)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/commanders/${commander.id}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
// Remove from local list
|
||||
this.commanders = this.commanders.filter(c => c.id !== commander.id)
|
||||
} else {
|
||||
this.serverError = 'Failed to delete commander'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete commander error:', error)
|
||||
this.serverError = 'Network error occurred'
|
||||
}
|
||||
},
|
||||
|
||||
// Form Management
|
||||
editCommander(commander) {
|
||||
this.editingCommander = { ...commander }
|
||||
this.editingCommander = JSON.parse(JSON.stringify(commander))
|
||||
if (!Array.isArray(this.editingCommander.colors)) {
|
||||
this.editingCommander.colors = []
|
||||
}
|
||||
this.editErrors = {}
|
||||
this.serverError = ''
|
||||
},
|
||||
@@ -255,78 +293,25 @@ function commanderManager() {
|
||||
this.newCommander = { name: '', colors: [] }
|
||||
this.errors = {}
|
||||
this.serverError = ''
|
||||
},
|
||||
|
||||
resetEditForm() {
|
||||
this.editingCommander = null
|
||||
this.editErrors = {}
|
||||
this.serverError = ''
|
||||
},
|
||||
|
||||
async debounceSearch() {
|
||||
this.$nextTick(() => {
|
||||
if (this.showPopular) {
|
||||
await this.loadCommanders()
|
||||
} else {
|
||||
await this.searchCommanders()
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
async searchCommanders() {
|
||||
if (!this.searchQuery.trim()) {
|
||||
await this.loadCommanders()
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
this.serverError = ''
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/commanders?q=${encodeURIComponent(this.searchQuery)}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('edh-stats-token') || sessionStorage.getItem('edh-stats-token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
this.commanders = data.commanders || []
|
||||
} else {
|
||||
this.serverError = 'Search failed'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Search commanders error:', error)
|
||||
this.serverError = 'Network error occurred'
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
// Global Utilities
|
||||
function getColorName(colorId) {
|
||||
const colorNames = {
|
||||
'W': 'White',
|
||||
'U': 'Blue',
|
||||
'B': 'Black',
|
||||
'R': 'Red',
|
||||
'G': 'Green'
|
||||
}
|
||||
return colorNames[colorId] || colorId
|
||||
const map = { 'W': 'White', 'U': 'Blue', 'B': 'Black', 'R': 'Red', 'G': 'Green' }
|
||||
return map[colorId] || colorId
|
||||
}
|
||||
|
||||
function formatDate(dateString) {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
||||
month: 'short', day: 'numeric', year: 'numeric'
|
||||
})
|
||||
}
|
||||
|
||||
// Make functions globally available
|
||||
// Register Alpine component
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('commanderManager', commanderManager)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user