diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index e8497dc..c2b6110 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -103,6 +103,7 @@ func setupRouter(cfg *config.Config) *gin.Engine { // WebSocket routes (handle auth internally) router.GET("/ws/matchmaking", websocket.MatchmakingHandler) + router.GET("/ws/gamification", websocket.GamificationWebSocketHandler) // Protected routes (JWT auth) auth := router.Group("/") @@ -112,6 +113,11 @@ func setupRouter(cfg *config.Config) *gin.Engine { auth.PUT("/user/updateprofile", routes.UpdateProfileRouteHandler) auth.GET("/leaderboard", routes.GetLeaderboardRouteHandler) auth.POST("/debate/result", routes.UpdateRatingAfterDebateRouteHandler) + + // Gamification routes + auth.POST("/api/award-badge", routes.AwardBadgeRouteHandler) + auth.POST("/api/update-score", routes.UpdateScoreRouteHandler) + auth.GET("/api/leaderboard", routes.GetGamificationLeaderboardRouteHandler) routes.SetupDebateVsBotRoutes(auth) // WebSocket signaling endpoint (handles auth internally) diff --git a/backend/controllers/auth.go b/backend/controllers/auth.go index eb6c602..3bf2e56 100644 --- a/backend/controllers/auth.go +++ b/backend/controllers/auth.go @@ -84,6 +84,9 @@ func GoogleLogin(ctx *gin.Context) { LastRatingUpdate: now, AvatarURL: avatarURL, IsVerified: true, + Score: 0, // Initialize gamification score + Badges: []string{}, // Initialize badges array + CurrentStreak: 0, // Initialize streak CreatedAt: now, UpdatedAt: now, } @@ -168,6 +171,9 @@ func SignUp(ctx *gin.Context) { Password: string(hashedPassword), IsVerified: false, VerificationCode: verificationCode, + Score: 0, // Initialize gamification score + Badges: []string{}, // Initialize badges array + CurrentStreak: 0, // Initialize streak CreatedAt: now, UpdatedAt: now, } diff --git a/backend/controllers/debatevsbot_controller.go b/backend/controllers/debatevsbot_controller.go index 7b30eed..869d805 100644 --- a/backend/controllers/debatevsbot_controller.go +++ b/backend/controllers/debatevsbot_controller.go @@ -1,7 +1,9 @@ package controllers import ( + "context" "encoding/json" + "log" "strings" "time" @@ -9,8 +11,10 @@ import ( "arguehub/models" "arguehub/services" "arguehub/utils" + "arguehub/websocket" "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) @@ -274,7 +278,239 @@ func JudgeDebate(c *gin.Context) { nil, ) + // Update gamification (score, badges, streaks) after bot debate + log.Printf("About to call updateGamificationAfterBotDebate for user %s, result: %s, topic: %s", + userID.Hex(), resultStatus, latestDebate.Topic) + + // Call synchronously but with recover to prevent panics from crashing the request + func() { + defer func() { + if r := recover(); r != nil { + log.Printf("Panic in updateGamificationAfterBotDebate: %v", r) + } + }() + updateGamificationAfterBotDebate(userID, resultStatus, latestDebate.Topic) + }() + c.JSON(200, JudgeResponse{ Result: result, }) } +// updateGamificationAfterBotDebate updates user score, checks for badges, and updates streaks after a bot debate +func updateGamificationAfterBotDebate(userID primitive.ObjectID, resultStatus, topic string) { + // Add recover to catch any panics + defer func() { + if r := recover(); r != nil { + log.Printf("Panic recovered in updateGamificationAfterBotDebate: %v", r) + } + }() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + log.Printf("Starting gamification update for user %s, result: %s", userID.Hex(), resultStatus) + + // Check if database is initialized + if db.MongoDatabase == nil { + log.Printf("ERROR: MongoDatabase is nil! Cannot update gamification.") + return + } + + userCollection := db.MongoDatabase.Collection("users") + log.Printf("User collection retrieved, attempting to find user %s", userID.Hex()) + + // Get current user to check existing badges and score + var user models.User + err := userCollection.FindOne(ctx, bson.M{"_id": userID}).Decode(&user) + if err != nil { + log.Printf("ERROR: Failed to get user for gamification update: %v (userID: %s)", err, userID.Hex()) + return + } + + log.Printf("Successfully retrieved user: %s (email: %s)", userID.Hex(), user.Email) + + log.Printf("Current user score: %d, badges: %v", user.Score, user.Badges) + + // Ensure score field exists - if it's 0 or not set, initialize it + // Note: MongoDB's $inc will create the field if it doesn't exist, but we'll ensure it's set + if user.Score < 0 { + // If score is negative (shouldn't happen), reset it to 0 + user.Score = 0 + } + + // Calculate points based on result + var pointsToAdd int + var action string + switch resultStatus { + case "win": + pointsToAdd = 50 // Points for winning against bot + action = "debate_win" + case "loss": + pointsToAdd = 10 // Participation points + action = "debate_loss" + case "draw": + pointsToAdd = 25 // Points for draw + action = "debate_complete" + default: + pointsToAdd = 10 // Default participation points + action = "debate_complete" + } + + log.Printf("Adding %d points for result: %s", pointsToAdd, resultStatus) + + // Update user score atomically - $inc will create the field if it doesn't exist + update := bson.M{ + "$inc": bson.M{"score": pointsToAdd}, + "$set": bson.M{"updatedAt": time.Now()}, + } + + // Use UpdateOne first to ensure the update happens + updateResult, err := userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update) + if err != nil { + log.Printf("Error updating score with UpdateOne: %v", err) + return + } + + if updateResult.MatchedCount == 0 { + log.Printf("User not found for score update: %s", userID.Hex()) + return + } + + // Now fetch the updated user + var updatedUser models.User + err = userCollection.FindOne(ctx, bson.M{"_id": userID}).Decode(&updatedUser) + if err != nil { + log.Printf("Error fetching updated user: %v", err) + return + } + + log.Printf("Successfully updated score. New score: %d (was %d, added %d)", + updatedUser.Score, user.Score, pointsToAdd) + + // Save score update record + scoreCollection := db.MongoDatabase.Collection("score_updates") + scoreUpdate := models.ScoreUpdate{ + ID: primitive.NewObjectID(), + UserID: userID, + Points: pointsToAdd, + Action: action, + CreatedAt: time.Now(), + Metadata: map[string]interface{}{ + "debateType": "user_vs_bot", + "topic": topic, + "result": resultStatus, + }, + } + _, err = scoreCollection.InsertOne(ctx, scoreUpdate) + if err != nil { + log.Printf("Error saving score update record: %v", err) + // Don't fail, continue with badge checks + } + + // Check for badges (FirstWin, etc.) + hasBadge := make(map[string]bool) + for _, badge := range updatedUser.Badges { + hasBadge[badge] = true + } + + // Check for FirstWin badge (first win against bot) + if resultStatus == "win" && !hasBadge["FirstWin"] { + badgeUpdate := bson.M{"$addToSet": bson.M{"badges": "FirstWin"}} + userCollection.UpdateOne(ctx, bson.M{"_id": userID}, badgeUpdate) + + // Update the updatedUser object to include the new badge + updatedUser.Badges = append(updatedUser.Badges, "FirstWin") + hasBadge["FirstWin"] = true + + // Save badge record + badgeCollection := db.MongoDatabase.Collection("user_badges") + userBadge := models.UserBadge{ + ID: primitive.NewObjectID(), + UserID: userID, + BadgeName: "FirstWin", + EarnedAt: time.Now(), + Metadata: map[string]interface{}{ + "debateType": "user_vs_bot", + "topic": topic, + }, + } + badgeCollection.InsertOne(ctx, userBadge) + + // Broadcast badge award via WebSocket + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: userID.Hex(), + BadgeName: "FirstWin", + Timestamp: time.Now(), + }) + log.Printf("Awarded FirstWin badge to user %s", userID.Hex()) + } + + // Check for automatic badges (Novice, Streak5, FactMaster, etc.) + checkAndAwardAutomaticBadges(ctx, userID, updatedUser) + + // Broadcast score update via WebSocket + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "score_updated", + UserID: userID.Hex(), + Points: pointsToAdd, + NewScore: updatedUser.Score, + Action: action, + Timestamp: time.Now(), + }) + + log.Printf("Updated gamification for user %s: +%d points (new score: %d), result: %s", + userID.Hex(), pointsToAdd, updatedUser.Score, resultStatus) +} + +// checkAndAwardAutomaticBadges checks if user qualifies for automatic badges +func checkAndAwardAutomaticBadges(ctx context.Context, userID primitive.ObjectID, user models.User) { + userCollection := db.MongoDatabase.Collection("users") + hasBadge := make(map[string]bool) + for _, badge := range user.Badges { + hasBadge[badge] = true + } + + // Check for Novice badge (first debate completed) + if user.Score >= 10 && !hasBadge["Novice"] { + update := bson.M{"$addToSet": bson.M{"badges": "Novice"}} + userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update) + + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: userID.Hex(), + BadgeName: "Novice", + Timestamp: time.Now(), + }) + } + + // Check for Streak5 badge (5 day streak) + if user.CurrentStreak >= 5 && !hasBadge["Streak5"] { + update := bson.M{"$addToSet": bson.M{"badges": "Streak5"}} + userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update) + + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: userID.Hex(), + BadgeName: "Streak5", + Timestamp: time.Now(), + }) + } + + // Check for FactMaster badge (high score threshold) + if user.Score >= 500 && !hasBadge["FactMaster"] { + update := bson.M{"$addToSet": bson.M{"badges": "FactMaster"}} + userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update) + + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: userID.Hex(), + BadgeName: "FactMaster", + Timestamp: time.Now(), + }) + } + + // Check for Debater10 badge (10 debates completed) + // Note: This would require tracking debate count, which might need to be added +} + diff --git a/backend/controllers/gamification_controller.go b/backend/controllers/gamification_controller.go new file mode 100644 index 0000000..aecff75 --- /dev/null +++ b/backend/controllers/gamification_controller.go @@ -0,0 +1,439 @@ +package controllers + +import ( + "context" + "fmt" + "log" + "net/http" + "time" + + "arguehub/db" + "arguehub/models" + "arguehub/websocket" + + "github.com/gin-gonic/gin" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo/options" +) + +// AwardBadgeRequest represents the request to award a badge +type AwardBadgeRequest struct { + BadgeName string `json:"badgeName" binding:"required"` + UserID string `json:"userId,omitempty"` // Optional, defaults to current user + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// UpdateScoreRequest represents the request to update a user's score +type UpdateScoreRequest struct { + Points int `json:"points" binding:"required"` + Action string `json:"action" binding:"required"` // "debate_complete", "win", "streak", etc. + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// Valid actions for score updates +var validActions = map[string]bool{ + "debate_complete": true, + "debate_win": true, + "debate_loss": true, + "streak": true, + "first_debate": true, + "participation": true, +} + +// Rate limit configuration: max requests per minute per action +const rateLimitWindow = 1 * time.Minute +const maxRequestsPerWindow = 10 + +// AwardBadge awards a badge to a user after validation +func AwardBadge(c *gin.Context) { + var req AwardBadgeRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) + return + } + + // Get current user ID from context (set by auth middleware) + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + currentUserID := userID.(primitive.ObjectID) + var targetUserID primitive.ObjectID + + // If userId is provided in request, validate it's the same user (or admin) + if req.UserID != "" { + var err error + targetUserID, err = primitive.ObjectIDFromHex(req.UserID) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid user ID"}) + return + } + // Users can only award badges to themselves unless they're admin + if targetUserID != currentUserID { + isAdmin, _ := c.Get("isAdmin") + if isAdmin != true { + c.JSON(http.StatusForbidden, gin.H{"error": "Cannot award badges to other users"}) + return + } + } + } else { + targetUserID = currentUserID + } + + // Validate badge name + validBadges := map[string]bool{ + "Novice": true, + "Streak5": true, + "FactMaster": true, + "FirstWin": true, + "Debater10": true, + } + + if !validBadges[req.BadgeName] { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid badge name"}) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Check if user already has this badge + userCollection := db.MongoDatabase.Collection("users") + var user models.User + err := userCollection.FindOne(ctx, bson.M{"_id": targetUserID}).Decode(&user) + if err != nil { + c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) + return + } + + // Check if badge already exists + for _, badge := range user.Badges { + if badge == req.BadgeName { + c.JSON(http.StatusConflict, gin.H{"error": "User already has this badge"}) + return + } + } + + // Add badge to user + update := bson.M{ + "$push": bson.M{"badges": req.BadgeName}, + "$set": bson.M{"updatedAt": time.Now()}, + } + + _, err = userCollection.UpdateOne(ctx, bson.M{"_id": targetUserID}, update) + if err != nil { + log.Printf("Error awarding badge: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to award badge"}) + return + } + + // Save badge record + badgeCollection := db.MongoDatabase.Collection("user_badges") + userBadge := models.UserBadge{ + ID: primitive.NewObjectID(), + UserID: targetUserID, + BadgeName: req.BadgeName, + EarnedAt: time.Now(), + Metadata: req.Metadata, + } + _, err = badgeCollection.InsertOne(ctx, userBadge) + if err != nil { + log.Printf("Error saving badge record: %v", err) + // Don't fail the request, badge was already awarded + } + + // Broadcast badge award via WebSocket + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: targetUserID.Hex(), + BadgeName: req.BadgeName, + Timestamp: time.Now(), + }) + + c.JSON(http.StatusOK, gin.H{ + "message": "Badge awarded successfully", + "badge": req.BadgeName, + "userId": targetUserID.Hex(), + }) +} + +// UpdateScore updates a user's score when they complete valid actions +func UpdateScore(c *gin.Context) { + var req UpdateScoreRequest + if err := c.ShouldBindJSON(&req); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request body", "details": err.Error()}) + return + } + + // Validate action + if !validActions[req.Action] { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid action"}) + return + } + + // Get current user ID from context + userID, exists := c.Get("userID") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + currentUserID := userID.(primitive.ObjectID) + + // Rate limiting check + if !checkRateLimit(currentUserID, req.Action) { + c.JSON(http.StatusTooManyRequests, gin.H{"error": "Rate limit exceeded. Please try again later."}) + return + } + + // Validate points (anti-cheat: reasonable limits) + if req.Points < 0 || req.Points > 1000 { + c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid points value"}) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + userCollection := db.MongoDatabase.Collection("users") + + // Update user score atomically + update := bson.M{ + "$inc": bson.M{"score": req.Points}, + "$set": bson.M{"updatedAt": time.Now()}, + } + + result := userCollection.FindOneAndUpdate( + ctx, + bson.M{"_id": currentUserID}, + update, + options.FindOneAndUpdate().SetReturnDocument(options.After), + ) + + var updatedUser models.User + if err := result.Decode(&updatedUser); err != nil { + log.Printf("Error updating score: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update score"}) + return + } + + // Save score update record + scoreCollection := db.MongoDatabase.Collection("score_updates") + scoreUpdate := models.ScoreUpdate{ + ID: primitive.NewObjectID(), + UserID: currentUserID, + Points: req.Points, + Action: req.Action, + CreatedAt: time.Now(), + Metadata: req.Metadata, + } + _, err := scoreCollection.InsertOne(ctx, scoreUpdate) + if err != nil { + log.Printf("Error saving score update record: %v", err) + // Don't fail the request + } + + // Check for automatic badge awards + checkAndAwardBadges(ctx, currentUserID, updatedUser) + + // Broadcast score update via WebSocket + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "score_updated", + UserID: currentUserID.Hex(), + Points: req.Points, + NewScore: updatedUser.Score, + Action: req.Action, + Timestamp: time.Now(), + }) + + c.JSON(http.StatusOK, gin.H{ + "message": "Score updated successfully", + "points": req.Points, + "newScore": updatedUser.Score, + }) +} + +// checkRateLimit verifies if a request should be rate limited +func checkRateLimit(userID primitive.ObjectID, action string) bool { + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + rateLimitCollection := db.MongoDatabase.Collection("rate_limits") + now := time.Now() + windowStart := now.Truncate(rateLimitWindow) + + // Find or create rate limit entry + filter := bson.M{ + "userId": userID, + "action": action, + "windowStart": windowStart, + } + + var entry models.RateLimitEntry + err := rateLimitCollection.FindOne(ctx, filter).Decode(&entry) + + if err != nil { + // No entry exists, create one + newEntry := models.RateLimitEntry{ + UserID: userID, + Action: action, + Count: 1, + WindowStart: windowStart, + } + rateLimitCollection.InsertOne(ctx, newEntry) + return true + } + + // Check if limit exceeded + if entry.Count >= maxRequestsPerWindow { + return false + } + + // Increment count + update := bson.M{"$inc": bson.M{"count": 1}} + rateLimitCollection.UpdateOne(ctx, filter, update) + + // Clean up old entries (background operation) + go cleanupOldRateLimits() + + return true +} + +// cleanupOldRateLimits removes rate limit entries older than the window +func cleanupOldRateLimits() { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cutoff := time.Now().Add(-rateLimitWindow * 2) + rateLimitCollection := db.MongoDatabase.Collection("rate_limits") + rateLimitCollection.DeleteMany(ctx, bson.M{"windowStart": bson.M{"$lt": cutoff}}) +} + +// checkAndAwardBadges checks if user qualifies for automatic badges +func checkAndAwardBadges(ctx context.Context, userID primitive.ObjectID, user models.User) { + userCollection := db.MongoDatabase.Collection("users") + hasBadge := make(map[string]bool) + for _, badge := range user.Badges { + hasBadge[badge] = true + } + + // Check for Novice badge (first debate completed) + if user.Score >= 10 && !hasBadge["Novice"] { + update := bson.M{"$push": bson.M{"badges": "Novice"}} + userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update) + + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: userID.Hex(), + BadgeName: "Novice", + Timestamp: time.Now(), + }) + } + + // Check for Streak5 badge (5 day streak) + if user.CurrentStreak >= 5 && !hasBadge["Streak5"] { + update := bson.M{"$push": bson.M{"badges": "Streak5"}} + userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update) + + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: userID.Hex(), + BadgeName: "Streak5", + Timestamp: time.Now(), + }) + } + + // Check for FactMaster badge (high score threshold, example: 500 points) + if user.Score >= 500 && !hasBadge["FactMaster"] { + update := bson.M{"$push": bson.M{"badges": "FactMaster"}} + userCollection.UpdateOne(ctx, bson.M{"_id": userID}, update) + + websocket.BroadcastGamificationEvent(models.GamificationEvent{ + Type: "badge_awarded", + UserID: userID.Hex(), + BadgeName: "FactMaster", + Timestamp: time.Now(), + }) + } +} + +// GetLeaderboard returns the top users based on their scores +func GetGamificationLeaderboard(c *gin.Context) { + // Check for authenticated user + currentemail, exists := c.Get("email") + if !exists { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"}) + return + } + + // Get limit from query params (default 50) + limit := 50 + if limitStr := c.Query("limit"); limitStr != "" { + if parsed, err := parseInt(limitStr); err == nil && parsed > 0 && parsed <= 100 { + limit = parsed + } + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + // Query users sorted by Score (descending) + collection := db.MongoDatabase.Collection("users") + findOptions := options.Find().SetSort(bson.D{{"score", -1}}).SetLimit(int64(limit)) + cursor, err := collection.Find(ctx, bson.M{}, findOptions) + if err != nil { + log.Printf("Failed to fetch users: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch leaderboard data"}) + return + } + defer cursor.Close(ctx) + + // Decode users into slice + var users []models.User + if err := cursor.All(ctx, &users); err != nil { + log.Printf("Failed to decode users: %v", err) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to decode leaderboard data"}) + return + } + + // Build debaters list + var debaters []Debater + for i, user := range users { + name := user.DisplayName + if name == "" { + name = user.Email // Fallback to email + } + + avatarURL := user.AvatarURL + if avatarURL == "" { + avatarURL = "https://api.dicebear.com/9.x/adventurer/svg?seed=" + name + } + + isCurrentUser := user.Email == currentemail + debaters = append(debaters, Debater{ + ID: user.ID.Hex(), + Rank: i + 1, + Name: name, + Score: user.Score, + Rating: int(user.Rating), + AvatarURL: avatarURL, + CurrentUser: isCurrentUser, + }) + } + + c.JSON(http.StatusOK, gin.H{ + "debaters": debaters, + "total": len(debaters), + }) +} + +// Helper function to parse int +func parseInt(s string) (int, error) { + var result int + _, err := fmt.Sscanf(s, "%d", &result) + return result, err +} + diff --git a/backend/controllers/leaderboard.go b/backend/controllers/leaderboard.go index 4680299..2c241df 100644 --- a/backend/controllers/leaderboard.go +++ b/backend/controllers/leaderboard.go @@ -1,8 +1,11 @@ package controllers import ( + "context" + "log" "net/http" "strconv" + "time" "arguehub/db" "arguehub/models" @@ -21,12 +24,13 @@ type LeaderboardData struct { // Debater represents a leaderboard entry type Debater struct { - ID string `json:"id"` - Rank int `json:"rank"` - Name string `json:"name"` - Score int `json:"score"` - AvatarURL string `json:"avatarUrl"` - CurrentUser bool `json:"currentUser"` + ID string `json:"id"` + Rank int `json:"rank"` + Name string `json:"name"` + Score int `json:"score"` + Rating int `json:"rating"` + AvatarURL string `json:"avatarUrl"` + CurrentUser bool `json:"currentUser"` } // Stat represents a single statistic @@ -80,7 +84,8 @@ func GetLeaderboard(c *gin.Context) { ID: user.ID.Hex(), Rank: i + 1, Name: name, - Score: int(user.Rating), + Score: user.Score, + Rating: int(user.Rating), AvatarURL: avatarURL, CurrentUser: isCurrentUser, }) @@ -88,11 +93,106 @@ func GetLeaderboard(c *gin.Context) { // Generate stats totalUsers := len(users) + ctx := context.Background() + + // Calculate DEBATES TODAY - count all debates created today + todayStart := time.Now().Truncate(24 * time.Hour) + todayEnd := todayStart.Add(24 * time.Hour) + + debatesToday := 0 + + // Count from saved_debate_transcripts + transcriptCollection := db.MongoDatabase.Collection("saved_debate_transcripts") + transcriptCount, err := transcriptCollection.CountDocuments(ctx, bson.M{ + "createdAt": bson.M{ + "$gte": todayStart, + "$lt": todayEnd, + }, + }) + if err == nil { + debatesToday += int(transcriptCount) + } + + // Count from debates_vs_bot (createdAt is int64 timestamp) + botDebateCollection := db.MongoDatabase.Collection("debates_vs_bot") + botDebateCount, err := botDebateCollection.CountDocuments(ctx, bson.M{ + "createdAt": bson.M{ + "$gte": todayStart.Unix(), + "$lt": todayEnd.Unix(), + }, + }) + if err == nil { + debatesToday += int(botDebateCount) + } + + // Count from team_debates + teamDebateCollection := db.MongoDatabase.Collection("team_debates") + teamDebateCount, err := teamDebateCollection.CountDocuments(ctx, bson.M{ + "createdAt": bson.M{ + "$gte": todayStart, + "$lt": todayEnd, + }, + }) + if err == nil { + debatesToday += int(teamDebateCount) + } + + // Count from debates collection (uses date field) + debateCollection := db.MongoDatabase.Collection("debates") + debateCount, err := debateCollection.CountDocuments(ctx, bson.M{ + "date": bson.M{ + "$gte": todayStart, + "$lt": todayEnd, + }, + }) + if err == nil { + debatesToday += int(debateCount) + } + + // Calculate DEBATING NOW - count active debates + debatingNow := 0 + + // Count active team debates + activeTeamDebates, err := teamDebateCollection.CountDocuments(ctx, bson.M{ + "status": "active", + }) + if err == nil { + debatingNow += int(activeTeamDebates) + } + + // Count debates with pending status (might be in progress) + pendingDebates, err := transcriptCollection.CountDocuments(ctx, bson.M{ + "result": "pending", + "updatedAt": bson.M{ + "$gte": time.Now().Add(-2 * time.Hour), // Active within last 2 hours + }, + }) + if err == nil { + debatingNow += int(pendingDebates) + } + + // Calculate EXPERTS ONLINE - users with high rating who have been active recently + // Consider users with rating >= 1500 as experts, and active within last 30 minutes + expertThreshold := 1500.0 + activeThreshold := time.Now().Add(-30 * time.Minute) + + expertsOnline, err := collection.CountDocuments(ctx, bson.M{ + "rating": bson.M{"$gte": expertThreshold}, + "$or": []bson.M{ + {"lastActivityDate": bson.M{"$gte": activeThreshold}}, + {"updatedAt": bson.M{"$gte": activeThreshold}}, + }, + }) + if err != nil { + log.Printf("Error counting experts online: %v", err) + expertsOnline = 0 + } + stats := []Stat{ {Icon: "crown", Value: strconv.Itoa(totalUsers), Label: "REGISTERED DEBATERS"}, - {Icon: "chessQueen", Value: "430", Label: "DEBATES TODAY"}, // Placeholder - {Icon: "medal", Value: "98", Label: "DEBATING NOW"}, // Placeholder - {Icon: "crown", Value: "37", Label: "EXPERTS ONLINE"}, // Placeholder + {Icon: "chessQueen", Value: strconv.Itoa(debatesToday), Label: "DEBATES TODAY"}, + {Icon: "medal", Value: strconv.Itoa(debatingNow), Label: "DEBATING NOW"}, + {Icon: "crown", Value: strconv.Itoa(int(expertsOnline)), Label: "EXPERTS ONLINE"}, } // Send response diff --git a/backend/controllers/profile_controller.go b/backend/controllers/profile_controller.go index c782089..d2282fd 100644 --- a/backend/controllers/profile_controller.go +++ b/backend/controllers/profile_controller.go @@ -190,12 +190,15 @@ func GetProfile(c *gin.Context) { "id": user.ID.Hex(), "displayName": displayName, "email": user.Email, - "bio": user.Bio, - "rating": int(user.Rating), - "twitter": user.Twitter, - "instagram": user.Instagram, - "linkedin": user.LinkedIn, - "avatarUrl": avatar, + "bio": user.Bio, + "rating": int(user.Rating), + "score": user.Score, + "badges": user.Badges, + "currentStreak": user.CurrentStreak, + "twitter": user.Twitter, + "instagram": user.Instagram, + "linkedin": user.LinkedIn, + "avatarUrl": avatar, }, "leaderboard": leaderboard, "stats": gin.H{ diff --git a/backend/controllers/team_controller.go b/backend/controllers/team_controller.go index 74d46fb..277c2f8 100644 --- a/backend/controllers/team_controller.go +++ b/backend/controllers/team_controller.go @@ -2,7 +2,8 @@ package controllers import ( "context" - "math/rand" + cryptoRand "crypto/rand" + "math/big" "net/http" "strings" "time" @@ -21,7 +22,14 @@ func generateTeamCode() string { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" b := make([]byte, 6) for i := range b { - b[i] = charset[rand.Intn(len(charset))] + n, err := cryptoRand.Int(cryptoRand.Reader, big.NewInt(int64(len(charset)))) + if err != nil { + // Fallback to time-based selection if cryptographic random fails + idx := (time.Now().UnixNano() + int64(i)) % int64(len(charset)) + b[i] = charset[int(idx)] + continue + } + b[i] = charset[n.Int64()] } return string(b) } @@ -202,6 +210,10 @@ func CreateTeam(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "You are already in a team. Leave your current team before creating a new one."}) return } + if err != nil && err != mongo.ErrNoDocuments { + c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to verify existing team membership"}) + return + } // Set captain information team.CaptainID = userID.(primitive.ObjectID) @@ -335,6 +347,16 @@ func JoinTeam(c *gin.Context) { return } + capacity := team.MaxSize + if capacity <= 0 { + capacity = 4 + } + + if len(team.Members) >= capacity { + c.JSON(http.StatusBadRequest, gin.H{"error": "Team is already full"}) + return + } + // Calculate new average Elo totalElo := 0.0 for _, member := range team.Members { @@ -591,7 +613,12 @@ func GetTeamMemberProfile(c *gin.Context) { func GetAvailableTeams(c *gin.Context) { collection := db.GetCollection("teams") cursor, err := collection.Find(context.Background(), bson.M{ - "$expr": bson.M{"$lt": []interface{}{bson.M{"$size": "$members"}, 4}}, // Teams with less than 4 members + "$expr": bson.M{ + "$lt": bson.A{ + bson.M{"$size": "$members"}, + "$maxSize", + }, + }, }) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve teams"}) diff --git a/backend/models/gamification.go b/backend/models/gamification.go new file mode 100644 index 0000000..768f606 --- /dev/null +++ b/backend/models/gamification.go @@ -0,0 +1,56 @@ +package models + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Badge represents a badge that can be earned +type Badge struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + Name string `bson:"name" json:"name"` + Description string `bson:"description" json:"description"` + Icon string `bson:"icon" json:"icon"` + Category string `bson:"category" json:"category"` // "achievement", "streak", "skill" + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` +} + +// UserBadge represents a badge earned by a user +type UserBadge struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + UserID primitive.ObjectID `bson:"userId" json:"userId"` + BadgeName string `bson:"badgeName" json:"badgeName"` + EarnedAt time.Time `bson:"earnedAt" json:"earnedAt"` + Metadata map[string]interface{} `bson:"metadata,omitempty" json:"metadata,omitempty"` // Optional metadata about how badge was earned +} + +// ScoreUpdate represents a score update event +type ScoreUpdate struct { + ID primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"` + UserID primitive.ObjectID `bson:"userId" json:"userId"` + Points int `bson:"points" json:"points"` + Action string `bson:"action" json:"action"` // "debate_complete", "win", "streak", etc. + CreatedAt time.Time `bson:"createdAt" json:"createdAt"` + Metadata map[string]interface{} `bson:"metadata,omitempty" json:"metadata,omitempty"` +} + +// RateLimitEntry tracks rate limiting for score updates +type RateLimitEntry struct { + UserID primitive.ObjectID `bson:"userId" json:"userId"` + Action string `bson:"action" json:"action"` + Count int `bson:"count" json:"count"` + WindowStart time.Time `bson:"windowStart" json:"windowStart"` +} + +// GamificationEvent represents a gamification event to broadcast via WebSocket +type GamificationEvent struct { + Type string `json:"type"` // "badge_awarded", "score_updated" + UserID string `json:"userId"` + BadgeName string `json:"badgeName,omitempty"` + Points int `json:"points,omitempty"` + NewScore int `json:"newScore,omitempty"` + Action string `json:"action,omitempty"` + Timestamp time.Time `json:"timestamp"` +} + diff --git a/backend/models/user.go b/backend/models/user.go index 44059a9..44e4111 100644 --- a/backend/models/user.go +++ b/backend/models/user.go @@ -27,4 +27,9 @@ type User struct { ResetPasswordCode string `bson:"resetPasswordCode,omitempty"` CreatedAt time.Time `bson:"createdAt"` UpdatedAt time.Time `bson:"updatedAt"` + // Gamification fields + Score int `bson:"score" json:"score"` // Total gamification score + Badges []string `bson:"badges,omitempty" json:"badges,omitempty"` // List of badge names earned + CurrentStreak int `bson:"currentStreak" json:"currentStreak"` // Current daily streak + LastActivityDate time.Time `bson:"lastActivityDate,omitempty" json:"lastActivityDate,omitempty"` // Last activity date for streak calculation } diff --git a/backend/routes/gamification.go b/backend/routes/gamification.go new file mode 100644 index 0000000..3959de5 --- /dev/null +++ b/backend/routes/gamification.go @@ -0,0 +1,20 @@ +package routes + +import ( + "arguehub/controllers" + + "github.com/gin-gonic/gin" +) + +func AwardBadgeRouteHandler(c *gin.Context) { + controllers.AwardBadge(c) +} + +func UpdateScoreRouteHandler(c *gin.Context) { + controllers.UpdateScore(c) +} + +func GetGamificationLeaderboardRouteHandler(c *gin.Context) { + controllers.GetGamificationLeaderboard(c) +} + diff --git a/backend/services/debatevsbot.go b/backend/services/debatevsbot.go index b43b32a..77b47fe 100644 --- a/backend/services/debatevsbot.go +++ b/backend/services/debatevsbot.go @@ -3,6 +3,7 @@ package services import ( "context" "fmt" + "log" "strings" "time" @@ -444,6 +445,7 @@ Provide ONLY the JSON output without any additional text.`, text, err := generateDefaultModelText(ctx, prompt) if err != nil || text == "" { if err != nil { + log.Printf("Gemini error: %v", err) } return "Unable to judge." } diff --git a/backend/services/gemini.go b/backend/services/gemini.go index 71e9605..e3c1c84 100644 --- a/backend/services/gemini.go +++ b/backend/services/gemini.go @@ -22,7 +22,15 @@ func generateModelText(ctx context.Context, modelName, prompt string) (string, e if geminiClient == nil { return "", errors.New("gemini client not initialized") } - resp, err := geminiClient.Models.GenerateContent(ctx, modelName, genai.Text(prompt), nil) + model := geminiClient.GenerativeModel(modelName) + model.SafetySettings = []*genai.SafetySetting{ + {Category: genai.HarmCategoryHarassment, Threshold: genai.HarmBlockNone}, + {Category: genai.HarmCategoryHateSpeech, Threshold: genai.HarmBlockNone}, + {Category: genai.HarmCategorySexuallyExplicit, Threshold: genai.HarmBlockNone}, + {Category: genai.HarmCategoryDangerousContent, Threshold: genai.HarmBlockNone}, + } + + resp, err := model.GenerateContent(ctx, genai.Text(prompt)) if err != nil { return "", err } diff --git a/backend/services/pros_cons.go b/backend/services/pros_cons.go index 3dbabff..d465555 100644 --- a/backend/services/pros_cons.go +++ b/backend/services/pros_cons.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "log" "strings" "arguehub/models" @@ -33,6 +34,7 @@ Examples: ctx := context.Background() response, err := generateDefaultModelText(ctx, prompt) if err != nil { + log.Printf("Failed to generate topic: %v", err) return getFallbackTopic(skillLevel), nil } if response == "" { diff --git a/backend/utils/auth.go b/backend/utils/auth.go index 5be57c9..c0052ba 100644 --- a/backend/utils/auth.go +++ b/backend/utils/auth.go @@ -35,6 +35,11 @@ func getJWTSecret() string { return jwtSecret } +// GetJWTSecret returns the JWT secret (public function for use in other packages) +func GetJWTSecret() string { + return getJWTSecret() +} + // ExtractNameFromEmail extracts the username before '@' func ExtractNameFromEmail(email string) string { re := regexp.MustCompile(`^([^@]+)`) diff --git a/backend/websocket/gamification.go b/backend/websocket/gamification.go new file mode 100644 index 0000000..e96f9cb --- /dev/null +++ b/backend/websocket/gamification.go @@ -0,0 +1,91 @@ +package websocket + +import ( + "log" + "sync" + + "arguehub/models" + + "github.com/gorilla/websocket" +) + +// GamificationClient represents a client connected for gamification updates +type GamificationClient struct { + Conn *websocket.Conn + UserID string + writeMu sync.Mutex +} + +// SafeWriteJSON safely writes JSON data to the gamification client's WebSocket connection +func (gc *GamificationClient) SafeWriteJSON(v interface{}) error { + gc.writeMu.Lock() + defer gc.writeMu.Unlock() + return gc.Conn.WriteJSON(v) +} + +// Global gamification hub for broadcasting events to all connected clients +var ( + gamificationClients = make(map[*GamificationClient]bool) + gamificationMutex sync.RWMutex +) + +// RegisterGamificationClient registers a client for gamification updates +func RegisterGamificationClient(client *GamificationClient) { + gamificationMutex.Lock() + defer gamificationMutex.Unlock() + gamificationClients[client] = true + log.Printf("Gamification client registered. Total clients: %d", len(gamificationClients)) +} + +// UnregisterGamificationClient removes a client from gamification updates +func UnregisterGamificationClient(client *GamificationClient) { + gamificationMutex.Lock() + defer gamificationMutex.Unlock() + delete(gamificationClients, client) + client.Conn.Close() + log.Printf("Gamification client unregistered. Total clients: %d", len(gamificationClients)) +} + +// BroadcastGamificationEvent broadcasts a gamification event to all connected clients +func BroadcastGamificationEvent(event models.GamificationEvent) { + gamificationMutex.RLock() + defer gamificationMutex.RUnlock() + + message := map[string]interface{}{ + "type": event.Type, + "userId": event.UserID, + "timestamp": event.Timestamp, + } + + if event.BadgeName != "" { + message["badgeName"] = event.BadgeName + } + if event.Points != 0 { + message["points"] = event.Points + } + if event.NewScore != 0 { + message["newScore"] = event.NewScore + } + if event.Action != "" { + message["action"] = event.Action + } + + // Broadcast to all connected clients + for client := range gamificationClients { + if err := client.SafeWriteJSON(message); err != nil { + log.Printf("Error broadcasting gamification event to client: %v", err) + // Remove client if write fails + go UnregisterGamificationClient(client) + } + } + + log.Printf("Broadcasted gamification event: %s to %d clients", event.Type, len(gamificationClients)) +} + +// GetGamificationClientsCount returns the number of connected gamification clients +func GetGamificationClientsCount() int { + gamificationMutex.RLock() + defer gamificationMutex.RUnlock() + return len(gamificationClients) +} + diff --git a/backend/websocket/gamification_handler.go b/backend/websocket/gamification_handler.go new file mode 100644 index 0000000..5851544 --- /dev/null +++ b/backend/websocket/gamification_handler.go @@ -0,0 +1,132 @@ +package websocket + +import ( + "context" + "log" + "net/http" + "strings" + "time" + + "arguehub/db" + "arguehub/models" + "arguehub/utils" + + "github.com/gin-gonic/gin" + "github.com/golang-jwt/jwt/v5" + "github.com/gorilla/websocket" + "go.mongodb.org/mongo-driver/bson" +) + +// GamificationWebSocketHandler handles WebSocket connections for gamification updates +func GamificationWebSocketHandler(c *gin.Context) { + // Get token from Authorization header or query parameter + var tokenString string + authz := c.GetHeader("Authorization") + if authz != "" { + // Extract token from header + tokenParts := strings.Split(authz, " ") + if len(tokenParts) == 2 && tokenParts[0] == "Bearer" { + tokenString = tokenParts[1] + } + } + + // Fallback to query parameter if header not present + if tokenString == "" { + tokenString = c.Query("token") + } + + if tokenString == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization token required"}) + return + } + + // Get JWT secret from utils + jwtSecret := utils.GetJWTSecret() + if jwtSecret == "" { + log.Printf("JWT secret not configured") + c.JSON(http.StatusInternalServerError, gin.H{"error": "Server configuration error"}) + return + } + + // Validate JWT token + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + return []byte(jwtSecret), nil + }) + + if err != nil || !token.Valid { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"}) + return + } + + claims, ok := token.Claims.(jwt.MapClaims) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token claims"}) + return + } + + email, ok := claims["sub"].(string) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token: missing email"}) + return + } + + // Get user from database + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + var user models.User + err = db.MongoDatabase.Collection("users").FindOne(ctx, bson.M{"email": email}).Decode(&user) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"}) + return + } + + // Upgrade connection to WebSocket + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Printf("WebSocket upgrade error: %v", err) + return + } + + // Create gamification client + client := &GamificationClient{ + Conn: conn, + UserID: user.ID.Hex(), + } + + // Register client + RegisterGamificationClient(client) + + // Send welcome message + welcomeMsg := map[string]interface{}{ + "type": "connected", + "message": "Connected to gamification updates", + "userId": user.ID.Hex(), + } + client.SafeWriteJSON(welcomeMsg) + + // Handle client disconnection + defer func() { + UnregisterGamificationClient(client) + }() + + // Keep connection alive and handle incoming messages (ping/pong) + for { + messageType, _, err := conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Printf("Gamification WebSocket error: %v", err) + } + break + } + + // Handle ping/pong for keepalive + if messageType == websocket.PingMessage { + if err := conn.WriteMessage(websocket.PongMessage, nil); err != nil { + log.Printf("Error writing pong: %v", err) + break + } + } + } +} + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4b0accb..310b430 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "lucide-react": "^0.446.0", "react": "^18.3.1", "react-avatar": "^5.0.4", + "react-confetti": "^6.4.0", "react-day-picker": "^9.7.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -69,7 +70,7 @@ "tsx": "^4.19.1", "typescript": "^5.6.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^7.2.2" } }, "node_modules/@alloc/quick-lru": { @@ -20861,6 +20862,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/netbsd-x64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", @@ -20912,6 +20930,23 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", @@ -25141,9 +25176,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", - "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "cpu": [ "arm" ], @@ -25155,9 +25190,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", - "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "cpu": [ "arm64" ], @@ -25169,9 +25204,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", "cpu": [ "arm64" ], @@ -25183,9 +25218,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", - "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", "cpu": [ "x64" ], @@ -25196,10 +25231,38 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", - "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "cpu": [ "arm" ], @@ -25211,9 +25274,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", - "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "cpu": [ "arm" ], @@ -25225,9 +25288,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", - "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "cpu": [ "arm64" ], @@ -25239,9 +25302,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", - "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "cpu": [ "arm64" ], @@ -25252,10 +25315,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", - "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "cpu": [ "ppc64" ], @@ -25267,9 +25344,23 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", - "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "cpu": [ "riscv64" ], @@ -25281,9 +25372,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", - "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "cpu": [ "s390x" ], @@ -25295,9 +25386,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "cpu": [ "x64" ], @@ -25309,9 +25400,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", "cpu": [ "x64" ], @@ -25322,10 +25413,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", - "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "cpu": [ "arm64" ], @@ -25337,9 +25442,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", - "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", "cpu": [ "ia32" ], @@ -25350,10 +25455,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", - "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "cpu": [ "x64" ], @@ -26954,9 +27073,9 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true, "license": "MIT" }, @@ -27022,14 +27141,20 @@ } }, "node_modules/@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "license": "MIT", "dependencies": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" } }, + "node_modules/@types/node/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.13", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", @@ -33681,9 +33806,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", "funding": [ { "type": "github", @@ -34450,9 +34575,9 @@ } }, "node_modules/postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -34469,8 +34594,8 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, "engines": { @@ -34814,6 +34939,21 @@ "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-confetti": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz", + "integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==", + "license": "MIT", + "dependencies": { + "tween-functions": "^1.2.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "react": "^16.3.0 || ^17.0.1 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-day-picker": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.7.0.tgz", @@ -35892,13 +36032,13 @@ } }, "node_modules/rollup": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -35908,22 +36048,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.22.5", - "@rollup/rollup-android-arm64": "4.22.5", - "@rollup/rollup-darwin-arm64": "4.22.5", - "@rollup/rollup-darwin-x64": "4.22.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", - "@rollup/rollup-linux-arm-musleabihf": "4.22.5", - "@rollup/rollup-linux-arm64-gnu": "4.22.5", - "@rollup/rollup-linux-arm64-musl": "4.22.5", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", - "@rollup/rollup-linux-riscv64-gnu": "4.22.5", - "@rollup/rollup-linux-s390x-gnu": "4.22.5", - "@rollup/rollup-linux-x64-gnu": "4.22.5", - "@rollup/rollup-linux-x64-musl": "4.22.5", - "@rollup/rollup-win32-arm64-msvc": "4.22.5", - "@rollup/rollup-win32-ia32-msvc": "4.22.5", - "@rollup/rollup-win32-x64-msvc": "4.22.5", + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", "fsevents": "~2.3.2" } }, @@ -37077,6 +37223,54 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -37678,6 +37872,12 @@ "@esbuild/win32-x64": "0.23.1" } }, + "node_modules/tween-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", + "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==", + "license": "BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -37900,6 +38100,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { @@ -38156,21 +38357,24 @@ } }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "esbuild": "^0.25.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -38179,19 +38383,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -38212,13 +38422,19 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -38229,13 +38445,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -38246,13 +38462,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -38263,13 +38479,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -38280,13 +38496,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -38297,13 +38513,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -38314,13 +38530,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -38331,13 +38547,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -38348,13 +38564,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -38365,13 +38581,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -38382,13 +38598,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -38399,13 +38615,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -38416,13 +38632,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -38433,13 +38649,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -38450,13 +38666,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -38467,13 +38683,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -38484,13 +38700,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -38501,13 +38717,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -38518,13 +38734,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -38535,13 +38768,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -38552,13 +38785,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -38569,13 +38802,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -38586,13 +38819,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -38603,13 +38836,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -38617,32 +38850,66 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/vlq": { @@ -54760,6 +55027,13 @@ "dev": true, "optional": true }, + "@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "dev": true, + "optional": true + }, "@esbuild/netbsd-x64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", @@ -54781,6 +55055,13 @@ "dev": true, "optional": true }, + "@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "dev": true, + "optional": true + }, "@esbuild/sunos-x64": { "version": "0.24.0", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", @@ -57162,114 +57443,156 @@ } }, "@rollup/rollup-android-arm-eabi": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.5.tgz", - "integrity": "sha512-SU5cvamg0Eyu/F+kLeMXS7GoahL+OoizlclVFX3l5Ql6yNlywJJ0OuqTzUx0v+aHhPHEB/56CT06GQrRrGNYww==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", + "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", "dev": true, "optional": true }, "@rollup/rollup-android-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.5.tgz", - "integrity": "sha512-S4pit5BP6E5R5C8S6tgU/drvgjtYW76FBuG6+ibG3tMvlD1h9LHVF9KmlmaUBQ8Obou7hEyS+0w+IR/VtxwNMQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", + "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", "dev": true, "optional": true }, "@rollup/rollup-darwin-arm64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", - "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", + "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", "dev": true, "optional": true }, "@rollup/rollup-darwin-x64": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.5.tgz", - "integrity": "sha512-D8brJEFg5D+QxFcW6jYANu+Rr9SlKtTenmsX5hOSzNYVrK5oLAEMTUgKWYJP+wdKyCdeSwnapLsn+OVRFycuQg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", + "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", + "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", + "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.5.tgz", - "integrity": "sha512-PNqXYmdNFyWNg0ma5LdY8wP+eQfdvyaBAojAXgO7/gs0Q/6TQJVXAXe8gwW9URjbS0YAammur0fynYGiWsKlXw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", + "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm-musleabihf": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.5.tgz", - "integrity": "sha512-kSSCZOKz3HqlrEuwKd9TYv7vxPYD77vHSUvM2y0YaTGnFc8AdI5TTQRrM1yIp3tXCKrSL9A7JLoILjtad5t8pQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", + "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.5.tgz", - "integrity": "sha512-oTXQeJHRbOnwRnRffb6bmqmUugz0glXaPyspp4gbQOPVApdpRrY/j7KP3lr7M8kTfQTyrBUzFjj5EuHAhqH4/w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", + "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", "dev": true, "optional": true }, "@rollup/rollup-linux-arm64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.5.tgz", - "integrity": "sha512-qnOTIIs6tIGFKCHdhYitgC2XQ2X25InIbZFor5wh+mALH84qnFHvc+vmWUpyX97B0hNvwNUL4B+MB8vJvH65Fw==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", + "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", "dev": true, "optional": true }, - "@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.5.tgz", - "integrity": "sha512-TMYu+DUdNlgBXING13rHSfUc3Ky5nLPbWs4bFnT+R6Vu3OvXkTkixvvBKk8uO4MT5Ab6lC3U7x8S8El2q5o56w==", + "@rollup/rollup-linux-loong64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", + "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-ppc64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", + "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", "dev": true, "optional": true }, "@rollup/rollup-linux-riscv64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.5.tgz", - "integrity": "sha512-PTQq1Kz22ZRvuhr3uURH+U/Q/a0pbxJoICGSprNLAoBEkyD3Sh9qP5I0Asn0y0wejXQBbsVMRZRxlbGFD9OK4A==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", + "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-musl": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", + "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", "dev": true, "optional": true }, "@rollup/rollup-linux-s390x-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.5.tgz", - "integrity": "sha512-bR5nCojtpuMss6TDEmf/jnBnzlo+6n1UhgwqUvRoe4VIotC7FG1IKkyJbwsT7JDsF2jxR+NTnuOwiGv0hLyDoQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", + "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-gnu": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.5.tgz", - "integrity": "sha512-N0jPPhHjGShcB9/XXZQWuWBKZQnC1F36Ce3sDqWpujsGjDz/CQtOL9LgTrJ+rJC8MJeesMWrMWVLKKNR/tMOCA==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", + "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", "dev": true, "optional": true }, "@rollup/rollup-linux-x64-musl": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.5.tgz", - "integrity": "sha512-uBa2e28ohzNNwjr6Uxm4XyaA1M/8aTgfF2T7UIlElLaeXkgpmIJ2EitVNQxjO9xLLLy60YqAgKn/AqSpCUkE9g==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", + "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-openharmony-arm64": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", + "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", "dev": true, "optional": true }, "@rollup/rollup-win32-arm64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.5.tgz", - "integrity": "sha512-RXT8S1HP8AFN/Kr3tg4fuYrNxZ/pZf1HemC5Tsddc6HzgGnJm0+Lh5rAHJkDuW3StI0ynNXukidROMXYl6ew8w==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", + "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", "dev": true, "optional": true }, "@rollup/rollup-win32-ia32-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.5.tgz", - "integrity": "sha512-ElTYOh50InL8kzyUD6XsnPit7jYCKrphmddKAe1/Ytt74apOxDq5YEcbsiKs0fR3vff3jEneMM+3I7jbqaMyBg==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", + "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-gnu": { + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", + "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", "dev": true, "optional": true }, "@rollup/rollup-win32-x64-msvc": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.5.tgz", - "integrity": "sha512-+lvL/4mQxSV8MukpkKyyvfwhH266COcWlXE/1qxwN08ajovta3459zrjLghYMgDerlzNwLAcFpvU+WWE5y6nAQ==", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", + "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", "dev": true, "optional": true }, @@ -58382,9 +58705,9 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" }, "@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, "@types/graceful-fs": { @@ -58442,11 +58765,18 @@ } }, "@types/node": { - "version": "22.7.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.4.tgz", - "integrity": "sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==", + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "requires": { - "undici-types": "~6.19.2" + "undici-types": "~6.21.0" + }, + "dependencies": { + "undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==" + } } }, "@types/prop-types": { @@ -62936,9 +63266,9 @@ "dev": true }, "nanoid": { - "version": "3.3.7", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", - "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==" + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, "natural-compare": { "version": "1.4.0", @@ -63439,12 +63769,12 @@ "dev": true }, "postcss": { - "version": "8.4.47", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", - "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "requires": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.0", + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, @@ -63636,6 +63966,14 @@ "md5": "^2.0.0" } }, + "react-confetti": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/react-confetti/-/react-confetti-6.4.0.tgz", + "integrity": "sha512-5MdGUcqxrTU26I2EU7ltkWPwxvucQTuqMm8dUz72z2YMqTD6s9vMcDUysk7n9jnC+lXuCPeJJ7Knf98VEYE9Rg==", + "requires": { + "tween-functions": "^1.2.0" + } + }, "react-day-picker": { "version": "9.7.0", "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.7.0.tgz", @@ -64341,28 +64679,34 @@ } }, "rollup": { - "version": "4.22.5", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", - "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", - "dev": true, - "requires": { - "@rollup/rollup-android-arm-eabi": "4.22.5", - "@rollup/rollup-android-arm64": "4.22.5", - "@rollup/rollup-darwin-arm64": "4.22.5", - "@rollup/rollup-darwin-x64": "4.22.5", - "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", - "@rollup/rollup-linux-arm-musleabihf": "4.22.5", - "@rollup/rollup-linux-arm64-gnu": "4.22.5", - "@rollup/rollup-linux-arm64-musl": "4.22.5", - "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", - "@rollup/rollup-linux-riscv64-gnu": "4.22.5", - "@rollup/rollup-linux-s390x-gnu": "4.22.5", - "@rollup/rollup-linux-x64-gnu": "4.22.5", - "@rollup/rollup-linux-x64-musl": "4.22.5", - "@rollup/rollup-win32-arm64-msvc": "4.22.5", - "@rollup/rollup-win32-ia32-msvc": "4.22.5", - "@rollup/rollup-win32-x64-msvc": "4.22.5", - "@types/estree": "1.0.6", + "version": "4.53.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", + "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.53.3", + "@rollup/rollup-android-arm64": "4.53.3", + "@rollup/rollup-darwin-arm64": "4.53.3", + "@rollup/rollup-darwin-x64": "4.53.3", + "@rollup/rollup-freebsd-arm64": "4.53.3", + "@rollup/rollup-freebsd-x64": "4.53.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", + "@rollup/rollup-linux-arm-musleabihf": "4.53.3", + "@rollup/rollup-linux-arm64-gnu": "4.53.3", + "@rollup/rollup-linux-arm64-musl": "4.53.3", + "@rollup/rollup-linux-loong64-gnu": "4.53.3", + "@rollup/rollup-linux-ppc64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-gnu": "4.53.3", + "@rollup/rollup-linux-riscv64-musl": "4.53.3", + "@rollup/rollup-linux-s390x-gnu": "4.53.3", + "@rollup/rollup-linux-x64-gnu": "4.53.3", + "@rollup/rollup-linux-x64-musl": "4.53.3", + "@rollup/rollup-openharmony-arm64": "4.53.3", + "@rollup/rollup-win32-arm64-msvc": "4.53.3", + "@rollup/rollup-win32-ia32-msvc": "4.53.3", + "@rollup/rollup-win32-x64-gnu": "4.53.3", + "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@types/estree": "1.0.8", "fsevents": "~2.3.2" } }, @@ -65160,6 +65504,31 @@ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, + "tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "requires": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "dependencies": { + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true + } + } + }, "title-case": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/title-case/-/title-case-3.0.3.tgz", @@ -65463,6 +65832,11 @@ } } }, + "tween-functions": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tween-functions/-/tween-functions-1.2.0.tgz", + "integrity": "sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -65587,7 +65961,8 @@ "undici-types": { "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -65746,208 +66121,234 @@ } }, "vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "7.2.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", + "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", "dev": true, "requires": { - "esbuild": "^0.21.3", + "esbuild": "^0.25.0", + "fdir": "^6.5.0", "fsevents": "~2.3.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" }, "dependencies": { "@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "dev": true, "optional": true }, "@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "dev": true, "optional": true }, "@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "dev": true, "optional": true }, "@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "dev": true, "optional": true }, "@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "dev": true, "optional": true }, "@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "dev": true, "optional": true }, "@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "dev": true, "optional": true }, "@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "dev": true, "optional": true }, "@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "dev": true, "optional": true }, "@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "dev": true, "optional": true }, "@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "dev": true, "optional": true }, "@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "dev": true, "optional": true }, "@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "dev": true, "optional": true }, "@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "dev": true, "optional": true }, "@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "dev": true, "optional": true }, "@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "dev": true, "optional": true }, "@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "dev": true, "optional": true }, "@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "dev": true, "optional": true }, "@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "dev": true, "optional": true }, "@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "dev": true, "optional": true }, "@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "dev": true, "optional": true }, "@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "dev": true, "optional": true }, "@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "dev": true, "optional": true }, "esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "requires": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "requires": {} + }, + "picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true } } }, diff --git a/frontend/package.json b/frontend/package.json index cba9c05..7e81067 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -34,6 +34,7 @@ "lucide-react": "^0.446.0", "react": "^18.3.1", "react-avatar": "^5.0.4", + "react-confetti": "^6.4.0", "react-day-picker": "^9.7.0", "react-dnd": "^16.0.1", "react-dnd-html5-backend": "^16.0.1", @@ -71,6 +72,6 @@ "tsx": "^4.19.1", "typescript": "^5.6.3", "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" + "vite": "^7.2.2" } } diff --git a/frontend/src/Pages/DebateRoom.tsx b/frontend/src/Pages/DebateRoom.tsx index 1632165..5581141 100644 --- a/frontend/src/Pages/DebateRoom.tsx +++ b/frontend/src/Pages/DebateRoom.tsx @@ -196,10 +196,24 @@ const turnTypes = [ ]; const extractJSON = (response: string): string => { + if (!response) return "{}"; + + // Try to extract JSON from markdown code fences const fenceRegex = /```(?:json)?\s*([\s\S]*?)\s*```/; const match = fenceRegex.exec(response); - if (match && match[1]) return match[1].trim(); - return response; + if (match && match[1]) { + return match[1].trim(); + } + + // Try to find JSON object in the response + const jsonMatch = response.match(/\{[\s\S]*\}/); + if (jsonMatch) { + return jsonMatch[0]; + } + + // If no JSON found, return empty object + console.warn("No JSON found in response:", response); + return "{}"; }; const DebateRoom: React.FC = () => { @@ -401,12 +415,12 @@ const DebateRoom: React.FC = () => { const nextStance = currentSequence[nextStep]; const nextEntity = currentState.userStance === nextStance ? "User" : "Bot"; - setState((prev) => ({ - ...prev, + setState({ + ...currentState, phaseStep: nextStep, isBotTurn: nextEntity === "Bot", timer: phases[currentState.currentPhase].time, - })); + }); setNextTurnPending(false); } else if (currentState.currentPhase < phases.length - 1) { const newPhase = currentState.currentPhase + 1; @@ -434,7 +448,7 @@ const DebateRoom: React.FC = () => { message: "Calculating scores and judging results...", isJudging: true, }); - setState((prev) => ({ ...prev, isDebateEnded: true })); + setState({ ...currentState, isDebateEnded: true }); judgeDebateResult(currentState.messages); setNextTurnPending(false); } @@ -502,14 +516,34 @@ const DebateRoom: React.FC = () => { phase: phases[state.currentPhase].name, }; - setState((prev) => ({ - ...prev, - messages: [...prev.messages, botMessage], - })); - - setNextTurnPending(true); + setState((prev) => { + const updatedState = { + ...prev, + messages: [...prev.messages, botMessage], + }; + // Advance turn after bot responds + setTimeout(() => { + advanceTurn(updatedState); + }, 100); // Small delay to ensure state is updated + return updatedState; + }); } catch (error) { - setNextTurnPending(true); + console.error("Bot error:", error); + // Even on error, advance turn to prevent getting stuck + setState((prev) => { + const errorMessage: Message = { + sender: "Bot", + text: "I encountered an error. Please continue.", + phase: phases[prev.currentPhase].name, + }; + const updatedState = { + ...prev, + messages: [...prev.messages, errorMessage], + isBotTurn: false, // Reset bot turn on error + }; + advanceTurn(updatedState); + return updatedState; + }); } finally { botTurnRef.current = false; } @@ -517,43 +551,77 @@ const DebateRoom: React.FC = () => { const judgeDebateResult = async (messages: Message[]) => { try { + console.log("Starting judgment with messages:", messages); const { result } = await judgeDebate({ history: messages, userId: debateData.userId, }); + console.log("Raw judge result:", result); + const jsonString = extractJSON(result); - const judgment: JudgmentData = JSON.parse(jsonString); + console.log("Extracted JSON string:", jsonString); + + let judgment: JudgmentData; + try { + judgment = JSON.parse(jsonString); + } catch (parseError) { + console.error("JSON parse error:", parseError, "Trying to fix JSON..."); + // Try to fix common JSON issues + const fixedJson = jsonString + .replace(/'/g, '"') // Replace single quotes with double quotes + .replace(/(\w+):/g, '"$1":') // Add quotes to keys + .replace(/,\s*}/g, '}') // Remove trailing commas + .replace(/,\s*]/g, ']'); // Remove trailing commas in arrays + try { + judgment = JSON.parse(fixedJson); + } catch (e) { + throw new Error(`Failed to parse JSON: ${e}`); + } + } + + console.log("Parsed judgment:", judgment); setJudgmentData(judgment); setPopup({ show: false, message: "" }); setShowJudgment(true); } catch (error) { + console.error("Judging error:", error); + // Show error to user + setPopup({ + show: true, + message: `Judgment error: ${error instanceof Error ? error.message : "Unknown error"}. Showing default results.`, + isJudging: false, + }); + + // Set default judgment data setJudgmentData({ opening_statement: { - user: { score: 0, reason: "Error" }, - bot: { score: 0, reason: "Error" }, + user: { score: 0, reason: "Error occurred during judgment" }, + bot: { score: 0, reason: "Error occurred during judgment" }, }, cross_examination: { - user: { score: 0, reason: "Error" }, - bot: { score: 0, reason: "Error" }, + user: { score: 0, reason: "Error occurred during judgment" }, + bot: { score: 0, reason: "Error occurred during judgment" }, }, answers: { - user: { score: 0, reason: "Error" }, - bot: { score: 0, reason: "Error" }, + user: { score: 0, reason: "Error occurred during judgment" }, + bot: { score: 0, reason: "Error occurred during judgment" }, }, closing: { - user: { score: 0, reason: "Error" }, - bot: { score: 0, reason: "Error" }, + user: { score: 0, reason: "Error occurred during judgment" }, + bot: { score: 0, reason: "Error occurred during judgment" }, }, total: { user: 0, bot: 0 }, verdict: { winner: "None", - reason: "Judgment failed", + reason: error instanceof Error ? error.message : "Judgment failed", congratulations: "", opponent_analysis: "", }, }); - setPopup({ show: false, message: "" }); - setShowJudgment(true); + setTimeout(() => { + setPopup({ show: false, message: "" }); + setShowJudgment(true); + }, 3000); } }; diff --git a/frontend/src/Pages/Leaderboard.tsx b/frontend/src/Pages/Leaderboard.tsx index f408055..d4a4d4a 100644 --- a/frontend/src/Pages/Leaderboard.tsx +++ b/frontend/src/Pages/Leaderboard.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { Table, TableHeader, @@ -11,9 +11,22 @@ import { } from "@/components/ui/table"; import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar"; import { Card } from "@/components/ui/card"; -import { FaCrown, FaMedal, FaChessQueen } from "react-icons/fa"; +import { + FaCrown, + FaMedal, + FaChessQueen, + FaRobot, + FaTrophy, +} from "react-icons/fa"; import { Button } from "@/components/ui/button"; import { fetchLeaderboardData } from "@/services/leaderboardService"; +import { + fetchGamificationLeaderboard, + createGamificationWebSocket, + GamificationEvent, +} from "@/services/gamificationService"; +import BadgeUnlocked from "@/components/BadgeUnlocked"; +import { useUser } from "@/hooks/useUser"; interface Debater { id: string; @@ -22,6 +35,7 @@ interface Debater { avatarUrl: string; name: string; score: number; + rating: number; } interface Stat { @@ -55,22 +69,46 @@ const mapIcon = (icon: string) => { } }; +type SortCategory = "score" | "rating" | null; + const Leaderboard: React.FC = () => { const [visibleCount, setVisibleCount] = useState(5); const [debaters, setDebaters] = useState([]); const [stats, setStats] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [badgeUnlocked, setBadgeUnlocked] = useState<{ + badgeName: string; + isOpen: boolean; + }>({ + badgeName: "", + isOpen: false, + }); + const [sortCategory, setSortCategory] = useState("score"); + const wsRef = useRef(null); + const { user } = useUser(); + // Load initial leaderboard data useEffect(() => { const loadData = async () => { try { setLoading(true); const token = localStorage.getItem("token"); if (!token) return; - const data: LeaderboardData = await fetchLeaderboardData(token); - setDebaters(data.debaters); - setStats(data.stats); + + // Try to fetch from gamification endpoint first, fallback to old endpoint + try { + const data = await fetchGamificationLeaderboard(token); + setDebaters(data.debaters); + // Keep stats from old endpoint for now + const oldData: LeaderboardData = await fetchLeaderboardData(token); + setStats(oldData.stats); + } catch { + // Fallback to old endpoint + const data: LeaderboardData = await fetchLeaderboardData(token); + setDebaters(data.debaters); + setStats(data.stats); + } } catch { setError("Failed to load leaderboard data. Please try again later."); } finally { @@ -81,21 +119,135 @@ const Leaderboard: React.FC = () => { loadData(); }, []); - const currentUserIndex = debaters.findIndex((debater) => debater.currentUser); + // Set up WebSocket connection for live updates + useEffect(() => { + const token = localStorage.getItem("token"); + if (!token || !user) return; + + // Clean up existing connection + if (wsRef.current) { + wsRef.current.close(); + } + + const ws = createGamificationWebSocket( + token, + (event: GamificationEvent) => { + console.log("Gamification event received:", event); + + if (event.type === "badge_awarded" && event.badgeName) { + // Show badge unlock notification if it's for the current user + setDebaters((currentDebaters) => { + const currentUserDebater = currentDebaters.find( + (d) => d.currentUser + ); + if ( + event.userId === currentUserDebater?.id || + event.userId === user?.id + ) { + setBadgeUnlocked({ + badgeName: event.badgeName!, + isOpen: true, + }); + } + return currentDebaters; + }); + } + + if (event.type === "score_updated") { + // Update the leaderboard when scores change + setDebaters((prevDebaters) => { + const updated = [...prevDebaters]; + const index = updated.findIndex((d) => d.id === event.userId); + + if (index !== -1 && event.newScore !== undefined) { + updated[index] = { + ...updated[index], + score: event.newScore, + }; + } + + return updated; + }); + + // Reload full leaderboard periodically to ensure accuracy + const reloadTimer = setTimeout(async () => { + try { + const token = localStorage.getItem("token"); + if (token) { + const data = await fetchGamificationLeaderboard(token); + setDebaters(data.debaters); + } + } catch (err) { + console.error("Error reloading leaderboard:", err); + } + }, 2000); + + return () => clearTimeout(reloadTimer); + } + }, + (error) => { + console.error("WebSocket error:", error); + }, + () => { + console.log("WebSocket closed"); + } + ); + + wsRef.current = ws; + + return () => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + }; + }, [user]); + + // Sort debaters based on selected category + const sortedDebaters = React.useMemo(() => { + const sorted = [...debaters]; + if (sortCategory) { + sorted.sort((a, b) => { + if (sortCategory === "score") { + return b.score - a.score; // Descending order + } else { + return b.rating - a.rating; // Descending order + } + }); + } + // Update ranks after sorting + sorted.forEach((debater, index) => { + debater.rank = index + 1; + }); + return sorted; + }, [debaters, sortCategory]); + + const currentUserIndex = sortedDebaters.findIndex( + (debater) => debater.currentUser + ); const getVisibleDebaters = () => { - if (!debaters.length) return []; - const initialList = debaters + if (!sortedDebaters.length) return []; + const initialList = sortedDebaters .filter((debater, index) => !debater.currentUser || index < visibleCount) .slice(0, visibleCount); if (currentUserIndex !== -1 && currentUserIndex >= visibleCount) { - return [...initialList.slice(0, -1), debaters[currentUserIndex]]; + return [...initialList.slice(0, -1), sortedDebaters[currentUserIndex]]; } return initialList; }; + const handleSortCategory = (category: SortCategory) => { + // If clicking the same category, toggle to null (no sort) or keep it + if (sortCategory === category) { + setSortCategory(null); + } else { + setSortCategory(category); + } + }; + const showMore = () => - setVisibleCount((prev) => Math.min(prev + 5, debaters.length)); + setVisibleCount((prev) => Math.min(prev + 5, sortedDebaters.length)); const visibleDebaters = getVisibleDebaters(); @@ -111,6 +263,11 @@ const Leaderboard: React.FC = () => { return (
+ setBadgeUnlocked({ badgeName: "", isOpen: false })} + />

Hone your skills and see how you stack up against top debaters! 🏆 @@ -128,8 +285,29 @@ const Leaderboard: React.FC = () => { Debater - - Score + handleSortCategory("score")} + > +

+ + VS BOT + {sortCategory === "score" && ( + + )} +
+ + handleSortCategory("rating")} + > +
+ + ELO RATING + {sortCategory === "rating" && ( + + )} +
@@ -187,13 +365,21 @@ const Leaderboard: React.FC = () => {
+ +
+ + {debater.rating} + +
+
+ ))} - {visibleCount < debaters.length && ( + {visibleCount < sortedDebaters.length && (
))}
-

- (Data fetched from backend) -

diff --git a/frontend/src/Pages/Profile.tsx b/frontend/src/Pages/Profile.tsx index e4278ef..dbd0b5b 100644 --- a/frontend/src/Pages/Profile.tsx +++ b/frontend/src/Pages/Profile.tsx @@ -49,7 +49,9 @@ import { X, Image as ImageIcon, ChevronRight, + Flame, } from "lucide-react"; +import { FaTrophy, FaMedal, FaAward } from "react-icons/fa"; import { format, isSameDay, subDays } from "date-fns"; import defaultAvatar from "@/assets/avatar2.jpg"; import { @@ -84,6 +86,9 @@ interface ProfileData { email: string; bio: string; rating: number; + score?: number; + badges?: string[]; + currentStreak?: number; twitter?: string; instagram?: string; linkedin?: string; @@ -707,6 +712,17 @@ const Profile: React.FC = () => {

Elo: {profile.rating}

+ {profile.score !== undefined && ( +

+ Score: {profile.score} +

+ )} + {profile.currentStreak !== undefined && profile.currentStreak > 0 && ( +

+ + Streak: {profile.currentStreak} days +

+ )}

@@ -736,12 +752,62 @@ const Profile: React.FC = () => { )} -

+

Bio

{renderBioField()}
+ +
+

+ Badges +

+ {profile.badges && profile.badges.length > 0 ? ( +
+ {profile.badges.map((badge, index) => { + const badgeIcons: Record = { + Novice: , + Streak5: , + FactMaster: , + FirstWin: , + Debater10: , + }; + const badgeDescriptions: Record = { + Novice: "Completed first debate", + Streak5: "5-day streak achieved", + FactMaster: "Master of facts (500+ points)", + FirstWin: "First victory earned", + Debater10: "10 debates completed", + }; + const badgeIcon = badgeIcons[badge] || ; + const badgeDescription = badgeDescriptions[badge] || "Achievement unlocked"; + + return ( +
+
+ {badgeIcon} +
+ + {badge} + +
+ ); + })} +
+ ) : ( +
+ +

+ No badges yet. Complete debates to earn badges! +

+
+ )} +
diff --git a/frontend/src/Pages/TeamDebateRoom.tsx b/frontend/src/Pages/TeamDebateRoom.tsx index 62992c7..b5c79d7 100644 --- a/frontend/src/Pages/TeamDebateRoom.tsx +++ b/frontend/src/Pages/TeamDebateRoom.tsx @@ -118,6 +118,10 @@ const phaseDurations: { [key in DebatePhase]?: number } = { [DebatePhase.ClosingAgainst]: 45, }; +const BASE_URL = + (import.meta.env.VITE_BASE_URL as string | undefined)?.replace(/\/$/, "") ?? + window.location.origin; + // Function to extract JSON from response const extractJSON = (response: string): string => { const fenceRegex = /```(?:json)?\s*([\s\S]*?)\s*```/; @@ -179,6 +183,11 @@ const TeamDebateRoom: React.FC = () => { const remoteVideoRefs = useRef>(new Map()); const localStreamRef = useRef(null); const debateStartedRef = useRef(false); // Track if debate has started to prevent popup reopening + const debatePhaseRef = useRef(debatePhase); + + useEffect(() => { + debatePhaseRef.current = debatePhase; + }, [debatePhase]); // State for media streams const [, setLocalStream] = useState(null); @@ -413,9 +422,12 @@ const TeamDebateRoom: React.FC = () => { userId: currentUser?.id, }); - const ws = new WebSocket( - `ws://localhost:1313/ws/team?debateId=${debateId}&token=${token}` - ); + const wsUrl = new URL("/ws/team", BASE_URL); + wsUrl.protocol = wsUrl.protocol === "https:" ? "wss:" : "ws:"; + wsUrl.searchParams.set("debateId", debateId); + wsUrl.searchParams.set("token", token); + + const ws = new WebSocket(wsUrl.toString()); wsRef.current = ws; ws.onopen = () => { @@ -431,7 +443,7 @@ const TeamDebateRoom: React.FC = () => { const currentPhase = debatePhaseRef.current; switch (data.type) { - case "stateSync": + case "stateSync": { // Sync all state when joining or receiving state update if (data.topic !== undefined) setTopic(data.topic); // CRITICAL: Only sync phase if debate hasn't started, or if backend phase is ahead @@ -440,6 +452,9 @@ const TeamDebateRoom: React.FC = () => { // Don't allow stateSync to reset phase back to Setup if debate has started if (debateStartedRef.current && backendPhase === DebatePhase.Setup) { } else { + console.log( + `stateSync: updating phase from ${debatePhaseRef.current} to ${backendPhase}` + ); setDebatePhase(backendPhase); // If backend says phase is not Setup, mark debate as started if (backendPhase !== DebatePhase.Setup) { @@ -531,7 +546,8 @@ const TeamDebateRoom: React.FC = () => { } break; - case "teamMembers": + } + case "teamMembers": { if (data.team1Members) { if (amTeam1) { setMyTeamMembers(data.team1Members); @@ -572,10 +588,11 @@ const TeamDebateRoom: React.FC = () => { }); } break; + } case "topicChange": if (data.topic !== undefined) setTopic(data.topic); break; - case "roleSelection": + case "roleSelection": { if (data.role && data.teamId) { // Determine if this is from our team or opponent team based on teamId const messageTeamId = data.teamId; @@ -593,18 +610,40 @@ const TeamDebateRoom: React.FC = () => { } } break; - case "countdownStart": + } + case "countdownStart": { // Backend is starting countdown - show it to all users const countdownValue = (data as any).countdown || 3; setCountdown(countdownValue); // Hide setup popup when countdown starts setShowSetupPopup(false); break; + } case "checkStart": // Ignore checkStart messages from backend (we shouldn't receive them) // This is sent by frontend to backend, not the other way around break; - case "ready": + case "ready": { + console.log("=== READY MESSAGE RECEIVED ==="); + console.log("Received ready message:", data); + console.log("Current user:", currentUser?.id); + console.log("Message userId:", data.userId); + console.log("Message teamId:", data.teamId); + console.log("Message assignedToTeam:", (data as any).assignedToTeam); + console.log("isTeam1:", isTeam1); + console.log("myTeamId:", myTeamId); + console.log( + "Team1Ready:", + data.team1Ready, + "Team2Ready:", + data.team2Ready + ); + console.log( + "Team1MembersCount:", + data.team1MembersCount, + "Team2MembersCount:", + data.team2MembersCount + ); // CRITICAL: Verify the ready status is assigned to the correct team const messageTeamId = data.teamId; @@ -679,13 +718,18 @@ const TeamDebateRoom: React.FC = () => { const allOppReady = oppReadyCount === oppTeamTotal && oppTeamTotal > 0; setPeerReady(allOppReady); break; - case "phaseChange": + } + case "phaseChange": { if (data.phase) { const newPhase = data.phase as DebatePhase; - + console.log( + `✓✓✓ RECEIVED PHASE CHANGE: ${newPhase} (previous: ${debatePhaseRef.current})` + ); + console.log(`Phase change data:`, data); + // Ensure we accept the phase change setDebatePhase(newPhase); - + // Close setup popup and clear countdown when debate starts (ALWAYS if not setup) if (newPhase !== DebatePhase.Setup) { debateStartedRef.current = true; // Mark debate as started - prevent popup from reopening @@ -696,9 +740,10 @@ const TeamDebateRoom: React.FC = () => { } else { } break; - case "speechText": + } + case "speechText": { if (data.userId && data.speechText) { - const targetPhase = data.phase || currentPhase; + const targetPhase = data.phase || debatePhaseRef.current; setSpeechTranscripts((prev) => ({ ...prev, [targetPhase]: @@ -706,7 +751,8 @@ const TeamDebateRoom: React.FC = () => { })); } break; - case "liveTranscript": + } + case "liveTranscript": { if ( data.userId && data.liveTranscript && @@ -715,7 +761,8 @@ const TeamDebateRoom: React.FC = () => { setCurrentTranscript(data.liveTranscript); } break; - case "teamStatus": + } + case "teamStatus": { // Update team member status if (data.team1Members) { if (amTeam1) { @@ -732,6 +779,7 @@ const TeamDebateRoom: React.FC = () => { } } break; + } case "offer": // Handle WebRTC offer break; diff --git a/frontend/src/components/BadgeUnlocked.tsx b/frontend/src/components/BadgeUnlocked.tsx new file mode 100644 index 0000000..d74ed70 --- /dev/null +++ b/frontend/src/components/BadgeUnlocked.tsx @@ -0,0 +1,94 @@ +import React, { useEffect, useState } from "react"; +import Confetti from "react-confetti"; +import { FaTrophy, FaMedal, FaAward } from "react-icons/fa"; +import { Dialog, DialogContent, DialogDescription, DialogTitle } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; + +interface BadgeUnlockedProps { + badgeName: string; + isOpen: boolean; + onClose: () => void; +} + +const badgeIcons: Record = { + Novice: , + Streak5: , + FactMaster: , + FirstWin: , + Debater10: , +}; + +const badgeDescriptions: Record = { + Novice: "You've completed your first debate! Welcome to DebateAI!", + Streak5: "Incredible! You've maintained a 5-day streak!", + FactMaster: "You're a master of facts! Keep up the great work!", + FirstWin: "Congratulations on your first victory!", + Debater10: "You've completed 10 debates! You're becoming a pro!", +}; + +const BadgeUnlocked: React.FC = ({ badgeName, isOpen, onClose }) => { + const [showConfetti, setShowConfetti] = useState(false); + const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); + + useEffect(() => { + if (isOpen) { + setShowConfetti(true); + setWindowSize({ width: window.innerWidth, height: window.innerHeight }); + + // Hide confetti after 5 seconds + const timer = setTimeout(() => { + setShowConfetti(false); + }, 5000); + + return () => clearTimeout(timer); + } + }, [isOpen]); + + useEffect(() => { + const handleResize = () => { + setWindowSize({ width: window.innerWidth, height: window.innerHeight }); + }; + + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + const badgeIcon = badgeIcons[badgeName] || ; + const badgeDescription = badgeDescriptions[badgeName] || "Congratulations on earning this badge!"; + + return ( + <> + {showConfetti && ( + + )} + + + + 🎉 Badge Unlocked! 🎉 + + +
+
{badgeIcon}
+

{badgeName}

+

{badgeDescription}

+
+
+
+ +
+
+
+ + ); +}; + +export default BadgeUnlocked; + diff --git a/frontend/src/components/JudgementPopup.tsx b/frontend/src/components/JudgementPopup.tsx index 68c6beb..1640dac 100644 --- a/frontend/src/components/JudgementPopup.tsx +++ b/frontend/src/components/JudgementPopup.tsx @@ -133,62 +133,64 @@ const JudgmentPopup: React.FC = ({ localStorage.getItem('opponentAvatar') || 'https://avatar.iran.liara.run/public/31'; - const isUserBotFormat = 'user' in judgment.opening_statement; - const defaultForName = forRole || 'For Debater'; - const defaultAgainstName = againstRole || 'Against Debater'; - const resolvedLocalName = localDisplayName || userName; - const resolvedOpponentName = opponentDisplayName || 'Opponent'; - const derivedLocalAvatar = localAvatarUrl || localAvatar; - const derivedOpponentAvatar = opponentAvatarUrl || opponentAvatar; - - const resolvedForName = isUserBotFormat - ? defaultForName - : localRole === 'for' - ? resolvedLocalName - : localRole === 'against' - ? resolvedOpponentName - : defaultForName; - - const resolvedAgainstName = isUserBotFormat - ? defaultAgainstName - : localRole === 'against' - ? resolvedLocalName - : localRole === 'for' - ? resolvedOpponentName - : defaultAgainstName; - - const player1Name = isUserBotFormat ? userName : resolvedForName; - const player2Name = isUserBotFormat ? botName || 'Bot' : resolvedAgainstName; - const player1Stance = isUserBotFormat ? userStance : 'For'; - const player2Stance = isUserBotFormat ? botStance : 'Against'; - - const resolvedForAvatar = isUserBotFormat - ? userAvatar - : localRole === 'for' - ? derivedLocalAvatar - : localRole === 'against' - ? derivedOpponentAvatar - : derivedOpponentAvatar || derivedLocalAvatar; - - const resolvedAgainstAvatar = isUserBotFormat - ? botAvatar - : localRole === 'against' - ? derivedLocalAvatar - : localRole === 'for' - ? derivedOpponentAvatar - : derivedLocalAvatar || derivedOpponentAvatar; - - const player1Avatar = resolvedForAvatar || localAvatar; - const player2Avatar = resolvedAgainstAvatar || opponentAvatar; - - const formatChange = (value: number) => - `${value >= 0 ? '+' : ''}${value.toFixed(2)}`; - const formatRating = (value: number) => value.toFixed(2); - - const player1RatingSummary = - !isUserBotFormat && ratingSummary ? ratingSummary.for : null; - const player2RatingSummary = - !isUserBotFormat && ratingSummary ? ratingSummary.against : null; +const isUserBotFormat = 'user' in judgment.opening_statement; + +const defaultForName = forRole || 'For Debater'; +const defaultAgainstName = againstRole || 'Against Debater'; +const resolvedLocalName = localDisplayName || userName; +const resolvedOpponentName = opponentDisplayName || 'Opponent'; +const derivedLocalAvatar = localAvatarUrl || localAvatar; +const derivedOpponentAvatar = opponentAvatarUrl || opponentAvatar; + +const resolvedForName = isUserBotFormat + ? defaultForName + : localRole === 'for' + ? resolvedLocalName + : localRole === 'against' + ? resolvedOpponentName + : defaultForName; + +const resolvedAgainstName = isUserBotFormat + ? defaultAgainstName + : localRole === 'against' + ? resolvedLocalName + : localRole === 'for' + ? resolvedOpponentName + : defaultAgainstName; + +const player1Name = isUserBotFormat ? userName : resolvedForName; +const player2Name = isUserBotFormat ? botName || 'Bot' : resolvedAgainstName; +const player1Stance = isUserBotFormat ? userStance : 'For'; +const player2Stance = isUserBotFormat ? botStance : 'Against'; + +const resolvedForAvatar = isUserBotFormat + ? userAvatar + : localRole === 'for' + ? derivedLocalAvatar + : localRole === 'against' + ? derivedOpponentAvatar + : derivedLocalAvatar || derivedOpponentAvatar; + +const resolvedAgainstAvatar = isUserBotFormat + ? botAvatar + : localRole === 'against' + ? derivedLocalAvatar + : localRole === 'for' + ? derivedOpponentAvatar + : derivedOpponentAvatar || derivedLocalAvatar; + +const player1Avatar = resolvedForAvatar || localAvatar; +const player2Avatar = resolvedAgainstAvatar || opponentAvatar; +const player2Desc = isUserBotFormat ? botDesc : resolvedAgainstName || 'Debater'; + +const formatChange = (value: number) => + `${value >= 0 ? '+' : ''}${value.toFixed(2)}`; +const formatRating = (value: number) => value.toFixed(2); + +const player1RatingSummary = + !isUserBotFormat && ratingSummary ? ratingSummary.for : null; +const player2RatingSummary = + !isUserBotFormat && ratingSummary ? ratingSummary.against : null; const handleGoHome = () => { navigate('/startdebate'); @@ -347,7 +349,7 @@ const JudgmentPopup: React.FC = ({ {player1Stance}

- {/*

Debater

*/} +

Debater

@@ -364,6 +366,9 @@ const JudgmentPopup: React.FC = ({ {player2Stance}

+

+ {player2Desc || 'Debater'} +

diff --git a/frontend/src/services/gamificationService.ts b/frontend/src/services/gamificationService.ts new file mode 100644 index 0000000..16c4a36 --- /dev/null +++ b/frontend/src/services/gamificationService.ts @@ -0,0 +1,130 @@ +const baseURL = import.meta.env.VITE_BASE_URL || "http://localhost:1313"; + +export interface GamificationEvent { + type: "badge_awarded" | "score_updated" | "connected"; + userId: string; + badgeName?: string; + points?: number; + newScore?: number; + action?: string; + timestamp: string; + message?: string; +} + +export interface AwardBadgeRequest { + badgeName: string; + userId?: string; + metadata?: Record; +} + +export interface UpdateScoreRequest { + points: number; + action: string; + metadata?: Record; +} + +export interface LeaderboardEntry { + id: string; + rank: number; + name: string; + score: number; + rating: number; + avatarUrl: string; + currentUser: boolean; +} + +export interface LeaderboardResponse { + debaters: LeaderboardEntry[]; + total: number; +} + +export const fetchGamificationLeaderboard = async (token: string): Promise => { + const response = await fetch(`${baseURL}/api/leaderboard`, { + method: "GET", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new Error(`Failed to fetch leaderboard: ${response.status}`); + } + + return response.json(); +}; + +export const awardBadge = async (token: string, request: AwardBadgeRequest) => { + const response = await fetch(`${baseURL}/api/award-badge`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || `Failed to award badge: ${response.status}`); + } + + return response.json(); +}; + +export const updateScore = async (token: string, request: UpdateScoreRequest) => { + const response = await fetch(`${baseURL}/api/update-score`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${token}`, + }, + body: JSON.stringify(request), + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.error || `Failed to update score: ${response.status}`); + } + + return response.json(); +}; + +export const createGamificationWebSocket = ( + token: string, + onMessage: (event: GamificationEvent) => void, + onError?: (error: Event) => void, + onClose?: () => void +): WebSocket => { + const target = new URL("/ws/gamification", baseURL); + target.protocol = target.protocol === "https:" ? "wss:" : "ws:"; + target.searchParams.set("token", token); + const wsURL = target.toString(); + const ws = new WebSocket(wsURL); + + ws.onopen = () => { + console.log("Connected to gamification WebSocket"); + }; + + ws.onmessage = (event) => { + try { + const data: GamificationEvent = JSON.parse(event.data); + onMessage(data); + } catch (error) { + console.error("Error parsing gamification event:", error); + } + }; + + ws.onerror = (error) => { + console.error("Gamification WebSocket error:", error); + if (onError) onError(error); + }; + + ws.onclose = () => { + console.log("Gamification WebSocket closed"); + if (onClose) onClose(); + }; + + return ws; +}; + diff --git a/frontend/src/services/teamDebateService.ts b/frontend/src/services/teamDebateService.ts index 1417454..964fb62 100644 --- a/frontend/src/services/teamDebateService.ts +++ b/frontend/src/services/teamDebateService.ts @@ -1,5 +1,15 @@ // Team debate service for API calls -const API_BASE_URL = "http://localhost:1313"; +const API_BASE_URL = + (import.meta.env.VITE_BASE_URL as string | undefined)?.replace(/\/$/, "") ?? + window.location.origin; + +interface TeamDebateMember { + userId: string; + email: string; + displayName: string; + avatarUrl?: string; + elo: number; +} function getAuthToken(): string { return localStorage.getItem("token") || "";