diff --git a/backend/models.py b/backend/models.py index b1feaaf..1399a3c 100644 --- a/backend/models.py +++ b/backend/models.py @@ -114,6 +114,8 @@ class Game(db.Model): current_question_index = db.Column(db.Integer, default=0) # Track current question 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 + completed_at = db.Column(db.DateTime, nullable=True) # When game ended + winners = db.Column(db.JSON, nullable=True) # [{"team_name": str, "score": int}, ...] # Relationships 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), 'is_template': self.is_template, '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: @@ -194,7 +198,7 @@ class Team(db.Model): phone_a_friend_count = db.Column(db.Integer, default=5) # Number of phone-a-friend lifelines # 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') @property diff --git a/backend/routes/teams.py b/backend/routes/teams.py index 799b8d5..fa9a706 100644 --- a/backend/routes/teams.py +++ b/backend/routes/teams.py @@ -4,6 +4,16 @@ from backend.models import db, Team 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('/', methods=['DELETE']) def delete_team(team_id): """Delete a team""" diff --git a/backend/services/game_service.py b/backend/services/game_service.py index dd19d11..893d581 100644 --- a/backend/services/game_service.py +++ b/backend/services/game_service.py @@ -203,20 +203,33 @@ def reset_timer(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.completed_at = datetime.utcnow() db.session.commit() - # Emit game_ended event to all rooms - socketio_instance.emit('game_ended', { + # Emit game_ended event with winners + end_data = { 'game_id': game.id, - 'game_name': game.name - }, room=f'game_{game.id}_contestant') - - socketio_instance.emit('game_ended', { - 'game_id': game.id, - 'game_name': game.name - }, room=f'game_{game.id}_admin') + 'game_name': game.name, + 'winners': game.winners + } + socketio_instance.emit('game_ended', end_data, room=f'game_{game.id}_contestant') + socketio_instance.emit('game_ended', end_data, room=f'game_{game.id}_admin') def restart_game(game, socketio_instance): diff --git a/frontend/frontend/src/components/questionbank/GameSetupView.jsx b/frontend/frontend/src/components/questionbank/GameSetupView.jsx index a2e5736..dc074e9 100644 --- a/frontend/frontend/src/components/questionbank/GameSetupView.jsx +++ b/frontend/frontend/src/components/questionbank/GameSetupView.jsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; 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"; export default function GameSetupView() { @@ -13,11 +13,24 @@ export default function GameSetupView() { const [showTemplateModal, setShowTemplateModal] = useState(false); const [templates, setTemplates] = useState([]); const [randomSelections, setRandomSelections] = useState({}); + const [pastTeamNames, setPastTeamNames] = useState([]); + const [showTeamSuggestions, setShowTeamSuggestions] = useState(false); + const teamInputRef = useRef(null); useEffect(() => { 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 () => { try { const response = await questionsAPI.getAll(); @@ -100,10 +113,12 @@ export default function GameSetupView() { setSelectedQuestions((prev) => prev.filter((id) => id !== questionId)); }; - const addTeam = () => { - if (newTeamName.trim()) { - setTeams([...teams, newTeamName.trim()]); + const addTeam = (name = newTeamName) => { + const teamName = name.trim(); + if (teamName && !teams.includes(teamName)) { + setTeams([...teams, teamName]); setNewTeamName(""); + setShowTeamSuggestions(false); } }; @@ -111,6 +126,18 @@ export default function GameSetupView() { 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) => { setRandomSelections((prev) => ({ ...prev, @@ -589,20 +616,97 @@ export default function GameSetupView() { 4. Add Teams
- setNewTeamName(e.target.value)} - onKeyPress={(e) => e.key === "Enter" && addTeam()} - placeholder="Enter team name" - style={{ padding: "0.5rem", flex: 1 }} - /> -
+ {quickAddSuggestions.length > 0 && ( +
+

+ Quick add from past games: +

+
+ {quickAddSuggestions.map((name) => ( + + ))} +
+
+ )}
{teams.map((team, index) => (
api.delete(`/teams/${id}`), + getPastNames: () => api.get("/teams/past-names"), }; // Admin API