Compare commits
2 Commits
e431ba45e9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02fcbad9ba | ||
|
|
d4345a4e09 |
147
MIGRATIONS.md
Normal file
147
MIGRATIONS.md
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# Database Migrations Guide
|
||||||
|
|
||||||
|
This document covers the proper workflow for database migrations and how to fix common issues.
|
||||||
|
|
||||||
|
## Migration Workflow Checklist
|
||||||
|
|
||||||
|
When making model changes, **always follow this order**:
|
||||||
|
|
||||||
|
### 1. Make model changes
|
||||||
|
Edit `backend/models.py` with your changes.
|
||||||
|
|
||||||
|
### 2. Create the migration
|
||||||
|
```bash
|
||||||
|
# Local development
|
||||||
|
uv run flask --app backend.app:create_app db migrate -m "Description of changes"
|
||||||
|
|
||||||
|
# Docker development
|
||||||
|
docker compose exec backend uv run flask --app backend.app:create_app db migrate -m "Description of changes"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Review the generated migration
|
||||||
|
Check `migrations/versions/` for the new file. Verify it does what you expect.
|
||||||
|
|
||||||
|
### 4. Apply locally and test
|
||||||
|
```bash
|
||||||
|
uv run flask --app backend.app:create_app db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Commit BOTH the model AND migration together
|
||||||
|
```bash
|
||||||
|
git add backend/models.py migrations/versions/
|
||||||
|
git commit -m "Add field_name to Model"
|
||||||
|
```
|
||||||
|
|
||||||
|
**CRITICAL: Never commit model changes without the corresponding migration file!**
|
||||||
|
|
||||||
|
### 6. Deploy and run migrations on production
|
||||||
|
```bash
|
||||||
|
# Rebuild to include new migration file
|
||||||
|
docker compose -f docker-compose.production.yml up --build -d
|
||||||
|
|
||||||
|
# Apply the migration
|
||||||
|
docker compose -f docker-compose.production.yml exec backend uv run flask --app backend.app:create_app db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues and Fixes
|
||||||
|
|
||||||
|
### Error: "Can't locate revision identified by 'xxxxx'"
|
||||||
|
|
||||||
|
**Cause:** The database references a migration that doesn't exist in the codebase (migration was applied but never committed).
|
||||||
|
|
||||||
|
**Fix:**
|
||||||
|
```bash
|
||||||
|
# 1. Check what revision the DB thinks it's at
|
||||||
|
docker compose -f docker-compose.production.yml exec backend python -c "
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('backend/instance/trivia.db')
|
||||||
|
print(conn.execute('SELECT * FROM alembic_version').fetchall())
|
||||||
|
"
|
||||||
|
|
||||||
|
# 2. Find the latest valid migration in your codebase
|
||||||
|
ls migrations/versions/
|
||||||
|
|
||||||
|
# 3. Force the DB to point to a valid migration
|
||||||
|
docker compose -f docker-compose.production.yml exec backend python -c "
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('backend/instance/trivia.db')
|
||||||
|
conn.execute(\"UPDATE alembic_version SET version_num = 'YOUR_VALID_REVISION'\")
|
||||||
|
conn.commit()
|
||||||
|
print('Done')
|
||||||
|
"
|
||||||
|
|
||||||
|
# 4. Manually add any missing columns
|
||||||
|
docker compose -f docker-compose.production.yml exec backend python -c "
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('backend/instance/trivia.db')
|
||||||
|
conn.execute('ALTER TABLE tablename ADD COLUMN columnname TYPE')
|
||||||
|
conn.commit()
|
||||||
|
print('Column added')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error: "table has no column named X"
|
||||||
|
|
||||||
|
**Cause:** Model has columns that don't exist in the database.
|
||||||
|
|
||||||
|
**Fix:** Add the columns manually:
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.production.yml exec backend python -c "
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('backend/instance/trivia.db')
|
||||||
|
cursor = conn.execute('PRAGMA table_info(tablename)')
|
||||||
|
print('Current columns:', [col[1] for col in cursor.fetchall()])
|
||||||
|
"
|
||||||
|
|
||||||
|
# Then add missing columns:
|
||||||
|
docker compose -f docker-compose.production.yml exec backend python -c "
|
||||||
|
import sqlite3
|
||||||
|
conn = sqlite3.connect('backend/instance/trivia.db')
|
||||||
|
conn.execute('ALTER TABLE games ADD COLUMN completed_at DATETIME')
|
||||||
|
conn.execute('ALTER TABLE games ADD COLUMN winners JSON')
|
||||||
|
conn.commit()
|
||||||
|
print('Done')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Checking current migration state
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# What migration does the DB think it's at?
|
||||||
|
docker compose -f docker-compose.production.yml exec backend uv run flask --app backend.app:create_app db current
|
||||||
|
|
||||||
|
# What's the latest migration in the codebase?
|
||||||
|
docker compose -f docker-compose.production.yml exec backend uv run flask --app backend.app:create_app db heads
|
||||||
|
|
||||||
|
# Show migration history
|
||||||
|
docker compose -f docker-compose.production.yml exec backend uv run flask --app backend.app:create_app db history
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nuclear option: Reset migrations (destroys all data!)
|
||||||
|
|
||||||
|
Only use if you can recreate all data:
|
||||||
|
```bash
|
||||||
|
# Delete database
|
||||||
|
docker compose -f docker-compose.production.yml exec backend rm -f backend/instance/trivia.db
|
||||||
|
|
||||||
|
# Recreate from scratch
|
||||||
|
docker compose -f docker-compose.production.yml exec backend uv run flask --app backend.app:create_app db upgrade
|
||||||
|
```
|
||||||
|
|
||||||
|
## SQLite Column Type Reference
|
||||||
|
|
||||||
|
When adding columns manually, use these SQLite types:
|
||||||
|
- `INTEGER` - for integers, booleans
|
||||||
|
- `TEXT` or `VARCHAR(N)` - for strings
|
||||||
|
- `DATETIME` - for dates/times
|
||||||
|
- `JSON` - for JSON data
|
||||||
|
- `FLOAT` or `REAL` - for decimals
|
||||||
|
|
||||||
|
## Pre-deployment Checklist
|
||||||
|
|
||||||
|
Before deploying model changes:
|
||||||
|
|
||||||
|
- [ ] Migration file exists in `migrations/versions/`
|
||||||
|
- [ ] Migration file is committed to git
|
||||||
|
- [ ] Migration tested locally with `db upgrade` and `db downgrade`
|
||||||
|
- [ ] Model changes and migration are in the same commit
|
||||||
@@ -114,6 +114,8 @@ class Game(db.Model):
|
|||||||
current_question_index = db.Column(db.Integer, default=0) # Track current question
|
current_question_index = db.Column(db.Integer, default=0) # Track current question
|
||||||
is_template = db.Column(db.Boolean, default=False) # Mark as reusable template
|
is_template = db.Column(db.Boolean, default=False) # Mark as reusable template
|
||||||
current_turn_team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True) # Track whose turn it is
|
current_turn_team_id = db.Column(db.Integer, db.ForeignKey('teams.id'), nullable=True) # Track whose turn it is
|
||||||
|
completed_at = db.Column(db.DateTime, nullable=True) # When game ended
|
||||||
|
winners = db.Column(db.JSON, nullable=True) # [{"team_name": str, "score": int}, ...]
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
teams = db.relationship('Team', back_populates='game', cascade='all, delete-orphan',
|
teams = db.relationship('Team', back_populates='game', cascade='all, delete-orphan',
|
||||||
@@ -146,7 +148,9 @@ class Game(db.Model):
|
|||||||
'total_questions': len(self.game_questions),
|
'total_questions': len(self.game_questions),
|
||||||
'is_template': self.is_template,
|
'is_template': self.is_template,
|
||||||
'current_turn_team_id': self.current_turn_team_id,
|
'current_turn_team_id': self.current_turn_team_id,
|
||||||
'current_turn_team_name': self.current_turn_team.name if self.current_turn_team else None
|
'current_turn_team_name': self.current_turn_team.name if self.current_turn_team else None,
|
||||||
|
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
||||||
|
'winners': self.winners
|
||||||
}
|
}
|
||||||
|
|
||||||
if include_questions:
|
if include_questions:
|
||||||
@@ -194,7 +198,7 @@ class Team(db.Model):
|
|||||||
phone_a_friend_count = db.Column(db.Integer, default=5) # Number of phone-a-friend lifelines
|
phone_a_friend_count = db.Column(db.Integer, default=5) # Number of phone-a-friend lifelines
|
||||||
|
|
||||||
# Relationships
|
# Relationships
|
||||||
game = db.relationship('Game', back_populates='teams')
|
game = db.relationship('Game', back_populates='teams', foreign_keys=[game_id])
|
||||||
scores = db.relationship('Score', back_populates='team', cascade='all, delete-orphan')
|
scores = db.relationship('Score', back_populates='team', cascade='all, delete-orphan')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -4,6 +4,16 @@ from backend.models import db, Team
|
|||||||
bp = Blueprint('teams', __name__, url_prefix='/api/teams')
|
bp = Blueprint('teams', __name__, url_prefix='/api/teams')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/past-names', methods=['GET'])
|
||||||
|
def get_past_team_names():
|
||||||
|
"""Get distinct team names from past games"""
|
||||||
|
past_names = db.session.query(Team.name)\
|
||||||
|
.distinct()\
|
||||||
|
.order_by(Team.name)\
|
||||||
|
.all()
|
||||||
|
return jsonify([name[0] for name in past_names]), 200
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/<int:team_id>', methods=['DELETE'])
|
@bp.route('/<int:team_id>', methods=['DELETE'])
|
||||||
def delete_team(team_id):
|
def delete_team(team_id):
|
||||||
"""Delete a team"""
|
"""Delete a team"""
|
||||||
|
|||||||
@@ -203,20 +203,33 @@ def reset_timer(game, socketio_instance):
|
|||||||
|
|
||||||
|
|
||||||
def end_game(game, socketio_instance):
|
def end_game(game, socketio_instance):
|
||||||
"""End/deactivate a game"""
|
"""End/deactivate a game and record winners"""
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
# Calculate winners (team(s) with highest score)
|
||||||
|
if game.teams:
|
||||||
|
team_scores = [(team.name, team.total_score) for team in game.teams]
|
||||||
|
if team_scores:
|
||||||
|
max_score = max(score for _, score in team_scores)
|
||||||
|
winners = [
|
||||||
|
{"team_name": name, "score": score}
|
||||||
|
for name, score in team_scores
|
||||||
|
if score == max_score
|
||||||
|
]
|
||||||
|
game.winners = winners
|
||||||
|
|
||||||
game.is_active = False
|
game.is_active = False
|
||||||
|
game.completed_at = datetime.utcnow()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
# Emit game_ended event to all rooms
|
# Emit game_ended event with winners
|
||||||
socketio_instance.emit('game_ended', {
|
end_data = {
|
||||||
'game_id': game.id,
|
'game_id': game.id,
|
||||||
'game_name': game.name
|
'game_name': game.name,
|
||||||
}, room=f'game_{game.id}_contestant')
|
'winners': game.winners
|
||||||
|
}
|
||||||
socketio_instance.emit('game_ended', {
|
socketio_instance.emit('game_ended', end_data, room=f'game_{game.id}_contestant')
|
||||||
'game_id': game.id,
|
socketio_instance.emit('game_ended', end_data, room=f'game_{game.id}_admin')
|
||||||
'game_name': game.name
|
|
||||||
}, room=f'game_{game.id}_admin')
|
|
||||||
|
|
||||||
|
|
||||||
def restart_game(game, socketio_instance):
|
def restart_game(game, socketio_instance):
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export default function ContestantView() {
|
|||||||
const [categories, setCategories] = useState([]);
|
const [categories, setCategories] = useState([]);
|
||||||
const [currentTurnTeamId, setCurrentTurnTeamId] = useState(null);
|
const [currentTurnTeamId, setCurrentTurnTeamId] = useState(null);
|
||||||
const [currentTurnTeamName, setCurrentTurnTeamName] = useState(null);
|
const [currentTurnTeamName, setCurrentTurnTeamName] = useState(null);
|
||||||
|
const [questionIndex, setQuestionIndex] = useState(0);
|
||||||
|
const [totalQuestions, setTotalQuestions] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Load initial game state
|
// Load initial game state
|
||||||
@@ -100,11 +102,14 @@ export default function ContestantView() {
|
|||||||
console.log("Game started:", data);
|
console.log("Game started:", data);
|
||||||
setGameName(data.game_name);
|
setGameName(data.game_name);
|
||||||
setGameStartTime(new Date());
|
setGameStartTime(new Date());
|
||||||
|
setTotalQuestions(data.total_questions);
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.on("question_changed", (data) => {
|
socket.on("question_changed", (data) => {
|
||||||
console.log("Question changed:", data);
|
console.log("Question changed:", data);
|
||||||
setCurrentQuestion(data.question);
|
setCurrentQuestion(data.question);
|
||||||
|
setQuestionIndex(data.question_index);
|
||||||
|
setTotalQuestions(data.total_questions);
|
||||||
setShowAnswer(false); // Hide answer when question changes
|
setShowAnswer(false); // Hide answer when question changes
|
||||||
setAnswer("");
|
setAnswer("");
|
||||||
setTimerSeconds(30);
|
setTimerSeconds(30);
|
||||||
@@ -339,6 +344,17 @@ export default function ContestantView() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{totalQuestions > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
color: "#999",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Question {questionIndex + 1} of {totalQuestions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{currentQuestion.category && (
|
{currentQuestion.category && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { questionsAPI, gamesAPI } from "../../services/api";
|
import { questionsAPI, gamesAPI, teamsAPI } from "../../services/api";
|
||||||
import AdminNavbar from "../common/AdminNavbar";
|
import AdminNavbar from "../common/AdminNavbar";
|
||||||
|
|
||||||
export default function GameSetupView() {
|
export default function GameSetupView() {
|
||||||
@@ -13,11 +13,24 @@ export default function GameSetupView() {
|
|||||||
const [showTemplateModal, setShowTemplateModal] = useState(false);
|
const [showTemplateModal, setShowTemplateModal] = useState(false);
|
||||||
const [templates, setTemplates] = useState([]);
|
const [templates, setTemplates] = useState([]);
|
||||||
const [randomSelections, setRandomSelections] = useState({});
|
const [randomSelections, setRandomSelections] = useState({});
|
||||||
|
const [pastTeamNames, setPastTeamNames] = useState([]);
|
||||||
|
const [showTeamSuggestions, setShowTeamSuggestions] = useState(false);
|
||||||
|
const teamInputRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuestions();
|
loadQuestions();
|
||||||
|
loadPastTeamNames();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadPastTeamNames = async () => {
|
||||||
|
try {
|
||||||
|
const response = await teamsAPI.getPastNames();
|
||||||
|
setPastTeamNames(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error loading past team names:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadQuestions = async () => {
|
const loadQuestions = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await questionsAPI.getAll();
|
const response = await questionsAPI.getAll();
|
||||||
@@ -100,10 +113,12 @@ export default function GameSetupView() {
|
|||||||
setSelectedQuestions((prev) => prev.filter((id) => id !== questionId));
|
setSelectedQuestions((prev) => prev.filter((id) => id !== questionId));
|
||||||
};
|
};
|
||||||
|
|
||||||
const addTeam = () => {
|
const addTeam = (name = newTeamName) => {
|
||||||
if (newTeamName.trim()) {
|
const teamName = name.trim();
|
||||||
setTeams([...teams, newTeamName.trim()]);
|
if (teamName && !teams.includes(teamName)) {
|
||||||
|
setTeams([...teams, teamName]);
|
||||||
setNewTeamName("");
|
setNewTeamName("");
|
||||||
|
setShowTeamSuggestions(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -111,6 +126,18 @@ export default function GameSetupView() {
|
|||||||
setTeams(teams.filter((_, i) => i !== index));
|
setTeams(teams.filter((_, i) => i !== index));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Filter past team names based on input and exclude already added teams
|
||||||
|
const filteredSuggestions = pastTeamNames.filter(
|
||||||
|
(name) =>
|
||||||
|
name.toLowerCase().includes(newTeamName.toLowerCase()) &&
|
||||||
|
!teams.includes(name)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get quick-add suggestions (past names not yet added to this game)
|
||||||
|
const quickAddSuggestions = pastTeamNames
|
||||||
|
.filter((name) => !teams.includes(name))
|
||||||
|
.slice(0, 8);
|
||||||
|
|
||||||
const updateRandomSelection = (category, count) => {
|
const updateRandomSelection = (category, count) => {
|
||||||
setRandomSelections((prev) => ({
|
setRandomSelections((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -589,20 +616,97 @@ export default function GameSetupView() {
|
|||||||
4. Add Teams
|
4. Add Teams
|
||||||
</h2>
|
</h2>
|
||||||
<div
|
<div
|
||||||
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem" }}
|
style={{ display: "flex", gap: "0.5rem", marginBottom: "1rem", position: "relative" }}
|
||||||
>
|
>
|
||||||
<input
|
<div style={{ flex: 1, position: "relative" }}>
|
||||||
type="text"
|
<input
|
||||||
value={newTeamName}
|
ref={teamInputRef}
|
||||||
onChange={(e) => setNewTeamName(e.target.value)}
|
type="text"
|
||||||
onKeyPress={(e) => e.key === "Enter" && addTeam()}
|
value={newTeamName}
|
||||||
placeholder="Enter team name"
|
onChange={(e) => {
|
||||||
style={{ padding: "0.5rem", flex: 1 }}
|
setNewTeamName(e.target.value);
|
||||||
/>
|
setShowTeamSuggestions(e.target.value.length > 0);
|
||||||
<button onClick={addTeam} style={{ padding: "0.5rem 1rem" }}>
|
}}
|
||||||
|
onFocus={() => setShowTeamSuggestions(newTeamName.length > 0)}
|
||||||
|
onBlur={() => {
|
||||||
|
// Delay hiding to allow click on suggestion
|
||||||
|
setTimeout(() => setShowTeamSuggestions(false), 200);
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => e.key === "Enter" && addTeam()}
|
||||||
|
placeholder="Enter team name"
|
||||||
|
style={{ padding: "0.5rem", width: "100%", boxSizing: "border-box" }}
|
||||||
|
/>
|
||||||
|
{showTeamSuggestions && filteredSuggestions.length > 0 && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "100%",
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
background: "white",
|
||||||
|
border: "1px solid #ccc",
|
||||||
|
borderRadius: "4px",
|
||||||
|
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
|
||||||
|
zIndex: 10,
|
||||||
|
maxHeight: "200px",
|
||||||
|
overflowY: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{filteredSuggestions.slice(0, 8).map((name) => (
|
||||||
|
<div
|
||||||
|
key={name}
|
||||||
|
onClick={() => addTeam(name)}
|
||||||
|
style={{
|
||||||
|
padding: "0.5rem 0.75rem",
|
||||||
|
cursor: "pointer",
|
||||||
|
borderBottom: "1px solid #eee",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => (e.target.style.background = "#f5f5f5")}
|
||||||
|
onMouseLeave={(e) => (e.target.style.background = "white")}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button onClick={() => addTeam()} style={{ padding: "0.5rem 1rem" }}>
|
||||||
Add Team
|
Add Team
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{quickAddSuggestions.length > 0 && (
|
||||||
|
<div style={{ marginBottom: "1rem" }}>
|
||||||
|
<p style={{ margin: "0 0 0.5rem 0", fontSize: "0.9rem", color: "#666" }}>
|
||||||
|
Quick add from past games:
|
||||||
|
</p>
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
|
||||||
|
{quickAddSuggestions.map((name) => (
|
||||||
|
<button
|
||||||
|
key={name}
|
||||||
|
onClick={() => addTeam(name)}
|
||||||
|
style={{
|
||||||
|
padding: "0.4rem 0.75rem",
|
||||||
|
background: "#f5f5f5",
|
||||||
|
border: "1px solid #ddd",
|
||||||
|
borderRadius: "16px",
|
||||||
|
cursor: "pointer",
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.target.style.background = "#e3f2fd";
|
||||||
|
e.target.style.borderColor = "#2196F3";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.target.style.background = "#f5f5f5";
|
||||||
|
e.target.style.borderColor = "#ddd";
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+ {name}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
|
<div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem" }}>
|
||||||
{teams.map((team, index) => (
|
{teams.map((team, index) => (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ export const gamesAPI = {
|
|||||||
// Teams API
|
// Teams API
|
||||||
export const teamsAPI = {
|
export const teamsAPI = {
|
||||||
delete: (id) => api.delete(`/teams/${id}`),
|
delete: (id) => api.delete(`/teams/${id}`),
|
||||||
|
getPastNames: () => api.get("/teams/past-names"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Admin API
|
// Admin API
|
||||||
|
|||||||
Reference in New Issue
Block a user