nextjs-rewrite #5

Merged
david merged 56 commits from nextjs-rewrite into main 2026-03-30 02:30:16 +00:00
3 changed files with 425 additions and 0 deletions
Showing only changes of commit eb8fcd3cf4 - Show all commits
+152
View File
@@ -0,0 +1,152 @@
/**
* Elo Rating Utilities
*
* Provides functions for calculating Elo ratings and changes
* Based on the standard Elo rating system
*/
/**
* Calculate the expected score for a player based on rating difference
* @param ratingA Player A's rating
* @param ratingB Player B's rating
* @returns Expected score (0-1) for Player A
*/
export function calculateExpectedScore(ratingA: number, ratingB: number): number {
const ratingDiff = ratingA - ratingB;
return 1 / (1 + Math.pow(10, -ratingDiff / 400));
}
/**
* Calculate Elo rating change for a single game
* @param rating Player's current rating
* @param opponentRating Opponent's rating
* @param actualScore Actual score (1 = win, 0.5 = draw, 0 = loss)
* @param kFactor K-factor (how much ratings change per game)
* @returns Elo rating change (positive or negative)
*/
export function calculateEloChange(
rating: number,
opponentRating: number,
actualScore: number,
kFactor: number = 32
): number {
const expectedScore = calculateExpectedScore(rating, opponentRating);
return kFactor * (actualScore - expectedScore);
}
/**
* Calculate team Elo rating as average of individual ratings
* @param rating1 Rating of player 1
* @param rating2 Rating of player 2
* @returns Team rating
*/
export function calculateTeamElo(rating1: number, rating2: number): number {
return (rating1 + rating2) / 2;
}
/**
* Update player statistics after a match
* @param currentRating Current player rating
* @param currentGamesPlayed Current games played count
* @param currentWins Current wins count
* @param eloChange Elo rating change
* @param won Whether the player won
* @returns Updated statistics
*/
export function updatePlayerStats(
currentRating: number,
currentGamesPlayed: number,
currentWins: number,
eloChange: number,
won: boolean
): {
newRating: number;
newGamesPlayed: number;
newWins: number;
newLosses: number;
winRate: number;
} {
const newRating = currentRating + Math.round(eloChange);
const newGamesPlayed = currentGamesPlayed + 1;
const newWins = currentWins + (won ? 1 : 0);
const newLosses = newGamesPlayed - newWins;
const winRate = newGamesPlayed > 0 ? newWins / newGamesPlayed : 0;
return {
newRating,
newGamesPlayed,
newWins,
newLosses,
winRate,
};
}
/**
* Calculate new rating after a match
* @param currentRating Current rating
* @param opponentRating Opponent's rating
* @param won Whether the player won
* @param kFactor K-factor (default 32)
* @returns New rating
*/
export function calculateNewRating(
currentRating: number,
opponentRating: number,
won: boolean,
kFactor: number = 32
): number {
const actualScore = won ? 1 : 0;
const eloChange = calculateEloChange(currentRating, opponentRating, actualScore, kFactor);
return Math.round(currentRating + eloChange);
}
/**
* Validate rating is within reasonable bounds
* @param rating Rating to validate
* @returns Whether the rating is valid
*/
export function isValidRating(rating: number): boolean {
return rating >= 100 && rating <= 3000;
}
/**
* Get rating tier name based on rating
* @param rating Player rating
* @returns Tier name
*/
export function getRatingTier(rating: number): string {
if (rating < 1100) return 'Beginner';
if (rating < 1300) return 'Novice';
if (rating < 1500) return 'Intermediate';
if (rating < 1700) return 'Advanced';
if (rating < 1900) return 'Expert';
if (rating < 2100) return 'Master';
return 'Grandmaster';
}
/**
* Calculate expected score for a team match
* @param team1Rating Average rating of team 1
* @param team2Rating Average rating of team 2
* @returns Expected score for team 1
*/
export function calculateExpectedTeamScore(team1Rating: number, team2Rating: number): number {
return calculateExpectedScore(team1Rating, team2Rating);
}
/**
* Calculate Elo change for team match
* @param team1Rating Average rating of team 1
* @param team2Rating Average rating of team 2
* @param team1Score Actual score for team 1 (1 = win, 0.5 = draw, 0 = loss)
* @param kFactor K-factor (default 32)
* @returns Elo change for team 1
*/
export function calculateTeamEloChange(
team1Rating: number,
team2Rating: number,
team1Score: number,
kFactor: number = 32
): number {
return calculateEloChange(team1Rating, team2Rating, team1Score, kFactor);
}
+217
View File
@@ -0,0 +1,217 @@
import { getSession } from './auth-simple';
import { prisma } from './prisma';
export type UserRole = 'player' | 'tournament_admin' | 'club_admin';
export interface PermissionCheck {
allowed: boolean;
reason?: string;
}
/**
* Check if the current user has the required role
* Reads role from database to avoid session cache issues
*/
export async function hasRole(requiredRole: UserRole): Promise<PermissionCheck> {
const session = await getSession();
if (!session) {
return { allowed: false, reason: 'Not authenticated' };
}
const userId = session.user?.id;
if (!userId) {
return { allowed: false, reason: 'User ID not found in session' };
}
// Fetch user from database to get current role (bypasses session cache)
const user = await prisma.user.findUnique({
where: { id: userId },
select: { role: true }
});
if (!user) {
return { allowed: false, reason: 'User not found in database' };
}
const userRole = user.role as UserRole;
// Role hierarchy: player < tournament_admin < club_admin
const roleHierarchy: Record<UserRole, number> = {
'player': 1,
'tournament_admin': 2,
'club_admin': 3
};
const hasRequiredRole = roleHierarchy[userRole] >= roleHierarchy[requiredRole];
return {
allowed: hasRequiredRole,
reason: hasRequiredRole ? undefined : `Requires ${requiredRole} role, but user has ${userRole}`
};
}
/**
* Check if the user owns the tournament (for tournament_admin)
*/
export async function ownsTournament(tournamentId: number): Promise<PermissionCheck> {
const session = await getSession();
if (!session) {
return { allowed: false, reason: 'Not authenticated' };
}
const userId = session.user?.id;
if (!userId) {
return { allowed: false, reason: 'User ID not found in session' };
}
const tournament = await prisma.event.findUnique({
where: { id: tournamentId },
select: { ownerId: true }
});
if (!tournament) {
return { allowed: false, reason: 'Tournament not found' };
}
const isOwner = tournament.ownerId === userId;
return {
allowed: isOwner,
reason: isOwner ? undefined : 'User does not own this tournament'
};
}
/**
* Check if the user can manage a specific tournament
* Tournament admins can only manage their own tournaments
* Club admins can manage all tournaments
* Reads role from database to avoid session cache issues
*/
export async function canManageTournament(tournamentId: number): Promise<PermissionCheck> {
const session = await getSession();
if (!session) {
return { allowed: false, reason: 'Not authenticated' };
}
const userId = session.user?.id;
if (!userId) {
return { allowed: false, reason: 'User ID not found in session' };
}
// Fetch user from database to get current role (bypasses session cache)
const user = await prisma.user.findUnique({
where: { id: userId },
select: { role: true }
});
if (!user) {
return { allowed: false, reason: 'User not found in database' };
}
const userRole = user.role as UserRole;
// Club admins can manage all tournaments
if (userRole === 'club_admin') {
return { allowed: true };
}
// Tournament admins can only manage their own tournaments
if (userRole === 'tournament_admin') {
return ownsTournament(tournamentId);
}
return { allowed: false, reason: 'Insufficient permissions to manage tournament' };
}
/**
* Check if the user can create tournaments
*/
export async function canCreateTournaments(): Promise<PermissionCheck> {
return hasRole('tournament_admin');
}
/**
* Check if the user can manage a specific match
* Tournament admins can manage matches in their own tournaments
* Club admins can manage all matches
*/
export async function canManageMatch(matchId: number): Promise<PermissionCheck> {
const session = await getSession();
if (!session) {
return { allowed: false, reason: 'Not authenticated' };
}
const userRole = session.user?.role as UserRole;
// Club admins can manage all matches
if (userRole === 'club_admin') {
return { allowed: true };
}
// Tournament admins can only manage matches in their own tournaments
if (userRole === 'tournament_admin') {
const match = await prisma.match.findUnique({
where: { id: matchId },
include: { event: true }
});
if (!match || !match.event) {
return { allowed: false, reason: 'Match or tournament not found' };
}
return ownsTournament(match.event.id);
}
return { allowed: false, reason: 'Insufficient permissions to manage match' };
}
/**
* Check if the user can assign tournament_admin privileges
* Only club admins can assign tournament_admin role
*/
export async function canAssignTournamentAdmin(): Promise<PermissionCheck> {
return hasRole('club_admin');
}
/**
* Get tournaments that the current user can manage
*/
export async function getManageableTournaments() {
const session = await getSession();
if (!session) {
return [];
}
const userRole = session.user?.role as UserRole;
const userId = session.user?.id;
if (userRole === 'club_admin') {
// Club admins can see all tournaments
return prisma.event.findMany({
where: { eventType: 'tournament' },
orderBy: { createdAt: 'desc' }
});
}
if (userRole === 'tournament_admin') {
// Tournament admins can only see their own tournaments
return prisma.event.findMany({
where: {
eventType: 'tournament',
ownerId: userId
},
orderBy: { createdAt: 'desc' }
});
}
// Regular players can only view tournaments (not manage them)
return prisma.event.findMany({
where: { eventType: 'tournament', status: { not: 'draft' } },
orderBy: { createdAt: 'desc' }
});
}
+56
View File
@@ -0,0 +1,56 @@
/**
* Tournament utility functions
* Used for calculating dynamic tournament status based on event date
*/
/**
* Calculates the tournament status based on the event date
* @param eventDate - The date of the tournament event
* @returns The calculated status string: "planned" or "completed"
*/
export function getTournamentStatus(eventDate: Date | null): string {
if (!eventDate) {
return "planned";
}
const now = new Date();
const eventDateObj = new Date(eventDate);
// If event date is in the past, it's completed
if (eventDateObj < now) {
return "completed";
}
// If event date is today or in the future, it's planned
return "planned";
}
/**
* Checks if a tournament date is in the past
* @param eventDate - The date of the tournament event
* @returns true if the event date is in the past, false otherwise
*/
export function isTournamentPast(eventDate: Date | null): boolean {
if (!eventDate) {
return false;
}
const now = new Date();
const eventDateObj = new Date(eventDate);
return eventDateObj < now;
}
/**
* Checks if a tournament date is in the future
* @param eventDate - The date of the tournament event
* @returns true if the event date is in the future, false otherwise
*/
export function isTournamentFuture(eventDate: Date | null): boolean {
if (!eventDate) {
return false;
}
const now = new Date();
const eventDateObj = new Date(eventDate);
return eventDateObj > now;
}