Refactor BSC Score to Astro, TypeScript, and modular architecture
This commit is contained in:
49
src/utils/constants.ts
Normal file
49
src/utils/constants.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { GameType } from '../types/game';
|
||||
|
||||
export const GAME_TYPES: Array<{ value: GameType; label: string; defaultRaceTo: number }> = [
|
||||
{ value: '8-Ball', label: '8-Ball', defaultRaceTo: 5 },
|
||||
{ value: '9-Ball', label: '9-Ball', defaultRaceTo: 9 },
|
||||
{ value: '10-Ball', label: '10-Ball', defaultRaceTo: 10 },
|
||||
{ value: '14/1 endlos', label: '14/1 Endlos', defaultRaceTo: 150 },
|
||||
];
|
||||
|
||||
export const RACE_TO_OPTIONS = [
|
||||
{ value: '5', label: 'Race to 5' },
|
||||
{ value: '7', label: 'Race to 7' },
|
||||
{ value: '9', label: 'Race to 9' },
|
||||
{ value: '11', label: 'Race to 11' },
|
||||
{ value: '15', label: 'Race to 15' },
|
||||
{ value: '25', label: 'Race to 25' },
|
||||
{ value: '50', label: 'Race to 50' },
|
||||
{ value: '100', label: 'Race to 100' },
|
||||
{ value: '150', label: 'Race to 150' },
|
||||
];
|
||||
|
||||
export const LOCAL_STORAGE_KEYS = {
|
||||
GAMES: 'bscscore_games',
|
||||
SETTINGS: 'bscscore_settings',
|
||||
PLAYER_HISTORY: 'bscscore_player_history',
|
||||
} as const;
|
||||
|
||||
export const APP_CONFIG = {
|
||||
MAX_PLAYER_NAME_LENGTH: 20,
|
||||
MAX_GAME_HISTORY: 100,
|
||||
MAX_PLAYER_HISTORY: 50,
|
||||
UNDO_STACK_SIZE: 10,
|
||||
} as const;
|
||||
|
||||
export const VALIDATION_MESSAGES = {
|
||||
PLAYER_NAME_REQUIRED: 'Spielername ist erforderlich',
|
||||
PLAYER_NAME_TOO_LONG: `Spielername darf maximal ${APP_CONFIG.MAX_PLAYER_NAME_LENGTH} Zeichen lang sein`,
|
||||
GAME_TYPE_REQUIRED: 'Spieltyp muss ausgewählt werden',
|
||||
RACE_TO_REQUIRED: 'Race-to Wert ist erforderlich',
|
||||
RACE_TO_INVALID: 'Race-to Wert muss eine positive Zahl sein',
|
||||
DUPLICATE_PLAYER_NAMES: 'Spielernamen müssen eindeutig sein',
|
||||
} as const;
|
||||
|
||||
export const BREAKPOINTS = {
|
||||
MOBILE: '480px',
|
||||
TABLET: '768px',
|
||||
DESKTOP: '1024px',
|
||||
LARGE_DESKTOP: '1200px',
|
||||
} as const;
|
||||
102
src/utils/gameUtils.ts
Normal file
102
src/utils/gameUtils.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { Game, StandardGame, EndlosGame } from '../types/game';
|
||||
|
||||
/**
|
||||
* Game utility functions for common operations
|
||||
*/
|
||||
|
||||
export function isEndlosGame(game: Game): game is EndlosGame {
|
||||
return 'players' in game;
|
||||
}
|
||||
|
||||
export function isStandardGame(game: Game): game is StandardGame {
|
||||
return !('players' in game);
|
||||
}
|
||||
|
||||
export function formatGameTime(dateString: string): string {
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffInHours = (now.getTime() - date.getTime()) / (1000 * 60 * 60);
|
||||
|
||||
if (diffInHours < 1) {
|
||||
return 'vor wenigen Minuten';
|
||||
} else if (diffInHours < 24) {
|
||||
return `vor ${Math.floor(diffInHours)} Stunde${Math.floor(diffInHours) !== 1 ? 'n' : ''}`;
|
||||
} else if (diffInHours < 168) { // 7 days
|
||||
const days = Math.floor(diffInHours / 24);
|
||||
return `vor ${days} Tag${days !== 1 ? 'en' : ''}`;
|
||||
} else {
|
||||
return date.toLocaleDateString('de-DE');
|
||||
}
|
||||
}
|
||||
|
||||
export function getGameDuration(game: Game): string {
|
||||
const start = new Date(game.createdAt);
|
||||
const end = new Date(game.updatedAt);
|
||||
const diffInMinutes = (end.getTime() - start.getTime()) / (1000 * 60);
|
||||
|
||||
if (diffInMinutes < 60) {
|
||||
return `${Math.floor(diffInMinutes)} Min`;
|
||||
} else {
|
||||
const hours = Math.floor(diffInMinutes / 60);
|
||||
const minutes = Math.floor(diffInMinutes % 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
export function calculateGameProgress(game: Game): number {
|
||||
if (isEndlosGame(game)) {
|
||||
const maxScore = Math.max(...game.players.map(p => p.score));
|
||||
return Math.min((maxScore / game.raceTo) * 100, 100);
|
||||
} else {
|
||||
const scores = [game.score1, game.score2, game.score3 || 0];
|
||||
const maxScore = Math.max(...scores);
|
||||
return Math.min((maxScore / game.raceTo) * 100, 100);
|
||||
}
|
||||
}
|
||||
|
||||
export function getGameWinner(game: Game): string | null {
|
||||
if (game.status !== 'completed') return null;
|
||||
|
||||
if ('winner' in game && game.winner) {
|
||||
return game.winner;
|
||||
}
|
||||
|
||||
if (isEndlosGame(game)) {
|
||||
const winner = game.players.find(player => player.score >= game.raceTo);
|
||||
return winner?.name || null;
|
||||
} else {
|
||||
if (game.score1 >= game.raceTo) return game.player1;
|
||||
if (game.score2 >= game.raceTo) return game.player2;
|
||||
if (game.player3 && game.score3 && game.score3 >= game.raceTo) return game.player3;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getGamePlayers(game: Game): Array<{ name: string; score: number }> {
|
||||
if (isEndlosGame(game)) {
|
||||
return game.players.map(player => ({
|
||||
name: player.name,
|
||||
score: player.score,
|
||||
}));
|
||||
} else {
|
||||
const players = [
|
||||
{ name: game.player1, score: game.score1 },
|
||||
{ name: game.player2, score: game.score2 },
|
||||
];
|
||||
if (game.player3) {
|
||||
players.push({ name: game.player3, score: game.score3 || 0 });
|
||||
}
|
||||
return players;
|
||||
}
|
||||
}
|
||||
|
||||
export function validateGameData(data: any): boolean {
|
||||
return !!(
|
||||
data.player1?.trim() &&
|
||||
data.player2?.trim() &&
|
||||
data.gameType &&
|
||||
data.raceTo &&
|
||||
parseInt(data.raceTo) > 0
|
||||
);
|
||||
}
|
||||
94
src/utils/validation.ts
Normal file
94
src/utils/validation.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { APP_CONFIG, VALIDATION_MESSAGES } from './constants';
|
||||
import type { NewGameData } from '../types/game';
|
||||
|
||||
export interface ValidationResult {
|
||||
isValid: boolean;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export function validatePlayerName(name: string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const trimmedName = name.trim();
|
||||
|
||||
if (!trimmedName) {
|
||||
errors.push(VALIDATION_MESSAGES.PLAYER_NAME_REQUIRED);
|
||||
}
|
||||
|
||||
if (trimmedName.length > APP_CONFIG.MAX_PLAYER_NAME_LENGTH) {
|
||||
errors.push(VALIDATION_MESSAGES.PLAYER_NAME_TOO_LONG);
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
export function validateGameData(data: NewGameData): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Validate player names
|
||||
const player1Validation = validatePlayerName(data.player1);
|
||||
const player2Validation = validatePlayerName(data.player2);
|
||||
|
||||
errors.push(...player1Validation.errors);
|
||||
errors.push(...player2Validation.errors);
|
||||
|
||||
// Check for duplicate player names
|
||||
const playerNames = [data.player1.trim(), data.player2.trim()];
|
||||
if (data.player3?.trim()) {
|
||||
const player3Validation = validatePlayerName(data.player3);
|
||||
errors.push(...player3Validation.errors);
|
||||
playerNames.push(data.player3.trim());
|
||||
}
|
||||
|
||||
const uniqueNames = new Set(playerNames.filter(name => name.length > 0));
|
||||
if (uniqueNames.size !== playerNames.filter(name => name.length > 0).length) {
|
||||
errors.push(VALIDATION_MESSAGES.DUPLICATE_PLAYER_NAMES);
|
||||
}
|
||||
|
||||
// Validate game type
|
||||
if (!data.gameType?.trim()) {
|
||||
errors.push(VALIDATION_MESSAGES.GAME_TYPE_REQUIRED);
|
||||
}
|
||||
|
||||
// Validate race to
|
||||
if (!data.raceTo?.trim()) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_REQUIRED);
|
||||
} else {
|
||||
const raceToNumber = parseInt(data.raceTo, 10);
|
||||
if (isNaN(raceToNumber) || raceToNumber <= 0) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
export function sanitizePlayerName(name: string): string {
|
||||
return name
|
||||
.trim()
|
||||
.slice(0, APP_CONFIG.MAX_PLAYER_NAME_LENGTH)
|
||||
.replace(/[^\w\s-]/g, ''); // Remove special characters except spaces and hyphens
|
||||
}
|
||||
|
||||
export function validateRaceTo(value: string): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!value?.trim()) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_REQUIRED);
|
||||
} else {
|
||||
const numValue = parseInt(value, 10);
|
||||
if (isNaN(numValue) || numValue <= 0) {
|
||||
errors.push(VALIDATION_MESSAGES.RACE_TO_INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user