refactor: consolidate game components and add toast notifications
- Remove EndlosGame support and GameDetail141.jsx component - Add Toast notification system with CSS styling - Refactor GameCompletionModal with enhanced styling - Improve GameDetail component structure and styling - Add BaseLayout.astro for consistent page structure - Update gameService with cleaner logic - Enhance global styles and remove unused constants - Streamline navigation components
This commit is contained in:
@@ -6,7 +6,7 @@ import { useNavigation, useNewGameWizard } from '../hooks/useNavigation';
|
|||||||
import { useModal, useValidationModal, useCompletionModal } from '../hooks/useModal';
|
import { useModal, useValidationModal, useCompletionModal } from '../hooks/useModal';
|
||||||
|
|
||||||
import { GameService } from '../services/gameService';
|
import { GameService } from '../services/gameService';
|
||||||
import type { StandardGame, EndlosGame } from '../types/game';
|
import type { StandardGame } from '../types/game';
|
||||||
|
|
||||||
import { Layout } from './ui/Layout';
|
import { Layout } from './ui/Layout';
|
||||||
import GameListScreen from './screens/GameListScreen';
|
import GameListScreen from './screens/GameListScreen';
|
||||||
@@ -36,42 +36,31 @@ export default function App() {
|
|||||||
navigation.showGameDetail(gameId);
|
navigation.showGameDetail(gameId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleUpdateScore = (player: number, change: number) => {
|
const handleUpdateScore = (player: number, change: number) => {
|
||||||
if (!navigation.currentGameId) return;
|
if (!navigation.currentGameId) return;
|
||||||
|
|
||||||
const game = gameState.getGameById(navigation.currentGameId);
|
const game = gameState.getGameById(navigation.currentGameId);
|
||||||
if (!game || game.status === 'completed' || 'players' in game) return;
|
if (!game || game.status === 'completed') return;
|
||||||
|
|
||||||
const updatedGame = GameService.updateGameScore(game as StandardGame, player, change);
|
const updatedGame = GameService.updateGameScore(game as StandardGame, player, change);
|
||||||
gameState.updateGame(navigation.currentGameId, updatedGame);
|
|
||||||
|
|
||||||
// Check for completion
|
// Add undo state for standard games
|
||||||
if (GameService.isGameCompleted(updatedGame)) {
|
|
||||||
completionModal.openCompletionModal(updatedGame);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGameAction = (updatedGame: EndlosGame) => {
|
|
||||||
if (!navigation.currentGameId) return;
|
|
||||||
|
|
||||||
const originalGame = gameState.getGameById(navigation.currentGameId);
|
|
||||||
if (!originalGame) return;
|
|
||||||
|
|
||||||
// Add undo state
|
|
||||||
const gameWithHistory = {
|
const gameWithHistory = {
|
||||||
...updatedGame,
|
...updatedGame,
|
||||||
undoStack: [...(originalGame.undoStack || []), originalGame],
|
undoStack: [...(game.undoStack || []), game],
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
gameState.updateGame(navigation.currentGameId, gameWithHistory);
|
gameState.updateGame(navigation.currentGameId, gameWithHistory);
|
||||||
|
|
||||||
// Check for completion
|
// Check for completion
|
||||||
if (GameService.isGameCompleted(gameWithHistory)) {
|
if (GameService.isGameCompleted(gameWithHistory)) {
|
||||||
completionModal.openCompletionModal(gameWithHistory);
|
completionModal.openCompletionModal(gameWithHistory);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const handleUndo = () => {
|
const handleUndo = () => {
|
||||||
if (!navigation.currentGameId) return;
|
if (!navigation.currentGameId) return;
|
||||||
|
|
||||||
@@ -181,10 +170,10 @@ export default function App() {
|
|||||||
onFilterChange={gameState.setFilter}
|
onFilterChange={gameState.setFilter}
|
||||||
onShowGameDetail={navigation.showGameDetail}
|
onShowGameDetail={navigation.showGameDetail}
|
||||||
onDeleteGame={handleDeleteGame}
|
onDeleteGame={handleDeleteGame}
|
||||||
onShowNewGame={() => {
|
onShowNewGame={() => {
|
||||||
newGameWizard.startWizard();
|
newGameWizard.startWizard();
|
||||||
navigation.showNewGame();
|
navigation.showNewGame();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -209,9 +198,7 @@ export default function App() {
|
|||||||
game={gameState.getGameById(navigation.currentGameId)}
|
game={gameState.getGameById(navigation.currentGameId)}
|
||||||
onUpdateScore={handleUpdateScore}
|
onUpdateScore={handleUpdateScore}
|
||||||
onFinishGame={handleFinishGame}
|
onFinishGame={handleFinishGame}
|
||||||
onUpdateGame={handleGameAction}
|
|
||||||
onUndo={handleUndo}
|
onUndo={handleUndo}
|
||||||
onForfeit={handleForfeit}
|
|
||||||
onBack={navigation.showGameList}
|
onBack={navigation.showGameList}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,56 +2,6 @@ import { h } from 'preact';
|
|||||||
import modalStyles from './Modal.module.css';
|
import modalStyles from './Modal.module.css';
|
||||||
import styles from './GameCompletionModal.module.css';
|
import styles from './GameCompletionModal.module.css';
|
||||||
|
|
||||||
const calculateStats = (game) => {
|
|
||||||
if (game.gameType !== '14/1 endlos' || !game.log) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = {};
|
|
||||||
game.players.forEach(p => {
|
|
||||||
stats[p.name] = {
|
|
||||||
totalPots: 0,
|
|
||||||
turnCount: 0,
|
|
||||||
highestRun: 0,
|
|
||||||
currentRun: 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const entry of game.log) {
|
|
||||||
if (entry.type === 'turn') {
|
|
||||||
const playerStats = stats[entry.player];
|
|
||||||
playerStats.turnCount += 1;
|
|
||||||
|
|
||||||
if (entry.ballsPotted > 0) {
|
|
||||||
playerStats.totalPots += entry.ballsPotted;
|
|
||||||
playerStats.currentRun += entry.ballsPotted;
|
|
||||||
} else {
|
|
||||||
// Run ends on a 0-pot turn
|
|
||||||
playerStats.highestRun = Math.max(playerStats.highestRun, playerStats.currentRun);
|
|
||||||
playerStats.currentRun = 0;
|
|
||||||
}
|
|
||||||
} else if (entry.type === 'foul') {
|
|
||||||
const playerStats = stats[entry.player];
|
|
||||||
playerStats.turnCount += 1;
|
|
||||||
// Run ends on a foul
|
|
||||||
playerStats.highestRun = Math.max(playerStats.highestRun, playerStats.currentRun);
|
|
||||||
playerStats.currentRun = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Final check for runs that extend to the end of the game
|
|
||||||
Object.values(stats).forEach(playerStats => {
|
|
||||||
playerStats.highestRun = Math.max(playerStats.highestRun, playerStats.currentRun);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Calculate averages
|
|
||||||
Object.keys(stats).forEach(playerName => {
|
|
||||||
const ps = stats[playerName];
|
|
||||||
ps.avgPots = ps.turnCount > 0 ? (ps.totalPots / ps.turnCount).toFixed(2) : 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return stats;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal shown when a game is completed.
|
* Modal shown when a game is completed.
|
||||||
@@ -66,16 +16,9 @@ const calculateStats = (game) => {
|
|||||||
const GameCompletionModal = ({ open, game, onConfirm, onClose, onRematch }) => {
|
const GameCompletionModal = ({ open, game, onConfirm, onClose, onRematch }) => {
|
||||||
if (!open || !game) return null;
|
if (!open || !game) return null;
|
||||||
|
|
||||||
let playerNames, scores, maxScore, winners, winnerText, gameStats;
|
const playerNames = [game.player1, game.player2, game.player3].filter(Boolean);
|
||||||
|
const scores = [game.score1, game.score2, game.score3].filter((_, i) => playerNames[i]);
|
||||||
if (game.gameType === '14/1 endlos') {
|
let maxScore, winners, winnerText;
|
||||||
playerNames = game.players.map(p => p.name);
|
|
||||||
scores = game.players.map(p => p.score);
|
|
||||||
gameStats = calculateStats(game);
|
|
||||||
} else {
|
|
||||||
playerNames = [game.player1, game.player2, game.player3].filter(Boolean);
|
|
||||||
scores = [game.score1, game.score2, game.score3].filter((_, i) => playerNames[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (game.forfeitedBy) {
|
if (game.forfeitedBy) {
|
||||||
winnerText = `${game.winner} hat gewonnen, da ${game.forfeitedBy} aufgegeben hat.`;
|
winnerText = `${game.winner} hat gewonnen, da ${game.forfeitedBy} aufgegeben hat.`;
|
||||||
@@ -105,19 +48,6 @@ const GameCompletionModal = ({ open, game, onConfirm, onClose, onRematch }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles['winner-announcement']}><h3>{winnerText}</h3></div>
|
<div className={styles['winner-announcement']}><h3>{winnerText}</h3></div>
|
||||||
|
|
||||||
{gameStats && (
|
|
||||||
<div className={styles['stats-container']}>
|
|
||||||
<h4 className={styles['stats-title']}>Statistiken</h4>
|
|
||||||
{playerNames.map(name => (
|
|
||||||
<div key={name} className={styles['player-stats']}>
|
|
||||||
<div className={styles['player-name-stats']}>{name}</div>
|
|
||||||
<div className={styles['stat-item']}>Höchste Serie: <strong>{gameStats[name].highestRun}</strong></div>
|
|
||||||
<div className={styles['stat-item']}>Punkte / Aufnahme: <strong>{gameStats[name].avgPots}</strong></div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className={modalStyles['modal-footer']}>
|
<div className={modalStyles['modal-footer']}>
|
||||||
<button className={styles['btn'] + ' ' + styles['btn--warning']} onClick={onConfirm} aria-label="Bestätigen">Bestätigen</button>
|
<button className={styles['btn'] + ' ' + styles['btn--warning']} onClick={onConfirm} aria-label="Bestätigen">Bestätigen</button>
|
||||||
|
|||||||
@@ -26,18 +26,68 @@
|
|||||||
.winner-announcement {
|
.winner-announcement {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin: 20px 0 0 0;
|
margin: 20px 0 0 0;
|
||||||
padding: 18px 8px;
|
padding: 24px 16px;
|
||||||
background: #43a047;
|
background: linear-gradient(135deg, #ff9800 0%, #ffa726 100%);
|
||||||
border-radius: 8px;
|
border-radius: 16px;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: #fff;
|
color: #222;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
box-shadow: 0 8px 32px rgba(255, 152, 0, 0.3);
|
||||||
|
animation: celebrationPulse 2s ease-in-out infinite;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.winner-announcement::before {
|
||||||
|
content: '🎉';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
left: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
animation: bounce 1s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.winner-announcement::after {
|
||||||
|
content: '🏆';
|
||||||
|
position: absolute;
|
||||||
|
top: -10px;
|
||||||
|
right: 20px;
|
||||||
|
font-size: 24px;
|
||||||
|
animation: bounce 1s ease-in-out infinite 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
.winner-announcement h3 {
|
.winner-announcement h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.5rem;
|
font-size: 1.8rem;
|
||||||
color: #2c3e50;
|
color: #222;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes celebrationPulse {
|
||||||
|
0%, 100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 8px 32px rgba(255, 152, 0, 0.3);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
box-shadow: 0 12px 40px rgba(255, 152, 0, 0.4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce {
|
||||||
|
0%, 20%, 50%, 80%, 100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
|
import { useState } from 'preact/hooks';
|
||||||
import styles from './GameDetail.module.css';
|
import styles from './GameDetail.module.css';
|
||||||
import GameDetail141 from './GameDetail141.jsx';
|
import Toast from './Toast.jsx';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Game detail view for a single game.
|
* Game detail view for a single game.
|
||||||
@@ -15,11 +16,21 @@ import GameDetail141 from './GameDetail141.jsx';
|
|||||||
* @returns {import('preact').VNode|null}
|
* @returns {import('preact').VNode|null}
|
||||||
*/
|
*/
|
||||||
const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, onForfeit, onBack }) => {
|
const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, onForfeit, onBack }) => {
|
||||||
|
const [toast, setToast] = useState({ show: false, message: '', type: 'info' });
|
||||||
|
|
||||||
if (!game) return null;
|
if (!game) return null;
|
||||||
|
|
||||||
if (game.gameType === '14/1 endlos') {
|
const showToast = (message, type = 'info') => {
|
||||||
return <GameDetail141 game={game} onUpdate={onUpdateGame} onUndo={onUndo} onForfeit={onForfeit} onBack={onBack} />;
|
setToast({ show: true, message, type });
|
||||||
}
|
};
|
||||||
|
|
||||||
|
const handleScoreUpdate = (playerIndex, change) => {
|
||||||
|
onUpdateScore(playerIndex, change);
|
||||||
|
const playerName = [game.player1, game.player2, game.player3][playerIndex - 1];
|
||||||
|
const action = change > 0 ? 'Punkt hinzugefügt' : 'Punkt abgezogen';
|
||||||
|
showToast(`${action} für ${playerName}`, 'success');
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const isCompleted = game.status === 'completed';
|
const isCompleted = game.status === 'completed';
|
||||||
const playerNames = [game.player1, game.player2, game.player3].filter(Boolean);
|
const playerNames = [game.player1, game.player2, game.player3].filter(Boolean);
|
||||||
@@ -31,39 +42,70 @@ const GameDetail = ({ game, onFinishGame, onUpdateScore, onUpdateGame, onUndo, o
|
|||||||
{game.gameType}{game.raceTo ? ` | Race to ${game.raceTo}` : ''}
|
{game.gameType}{game.raceTo ? ` | Race to ${game.raceTo}` : ''}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['scores-container']}>
|
<div className={styles['scores-container']}>
|
||||||
{playerNames.map((name, idx) => (
|
{playerNames.map((name, idx) => {
|
||||||
<div
|
const currentScore = scores[idx];
|
||||||
className={styles['player-score'] + (name === 'Fränky' ? ' ' + styles['franky'] : '')}
|
const progressPercentage = game.raceTo ? Math.min((currentScore / game.raceTo) * 100, 100) : 0;
|
||||||
key={name + idx}
|
|
||||||
>
|
return (
|
||||||
<span className={styles['player-name']}>{name}</span>
|
<div
|
||||||
<span
|
className={styles['player-score'] + (name === 'Fränky' ? ' ' + styles['franky'] : '')}
|
||||||
className={styles['score']}
|
key={name + idx}
|
||||||
id={`score${idx + 1}`}
|
|
||||||
onClick={() => !isCompleted && onUpdateScore(idx + 1, 1)}
|
|
||||||
aria-label={`Aktueller Punktestand für ${name}: ${scores[idx]}. Klicken zum Erhöhen.`}
|
|
||||||
>
|
>
|
||||||
{scores[idx]}
|
<span className={styles['player-name']}>{name}</span>
|
||||||
</span>
|
<div className={styles['progress-bar']}>
|
||||||
<button
|
<div
|
||||||
className={styles['score-button']}
|
className={styles['progress-fill']}
|
||||||
disabled={isCompleted}
|
style={{ width: `${progressPercentage}%` }}
|
||||||
onClick={() => onUpdateScore(idx+1, -1)}
|
/>
|
||||||
aria-label={`Punkt abziehen für ${name}`}
|
</div>
|
||||||
>-</button>
|
<span
|
||||||
<button
|
className={styles['score']}
|
||||||
className={styles['score-button']}
|
id={`score${idx + 1}`}
|
||||||
disabled={isCompleted}
|
onClick={() => !isCompleted && onUpdateScore(idx + 1, 1)}
|
||||||
onClick={() => onUpdateScore(idx+1, 1)}
|
aria-label={`Aktueller Punktestand für ${name}: ${scores[idx]}. Klicken zum Erhöhen.`}
|
||||||
aria-label={`Punkt hinzufügen für ${name}`}
|
>
|
||||||
>+</button>
|
{scores[idx]}
|
||||||
</div>
|
</span>
|
||||||
))}
|
<div className={styles['score-buttons']}>
|
||||||
|
<button
|
||||||
|
className={styles['score-button']}
|
||||||
|
disabled={isCompleted}
|
||||||
|
onClick={() => handleScoreUpdate(idx+1, -1)}
|
||||||
|
aria-label={`Punkt abziehen für ${name}`}
|
||||||
|
>-</button>
|
||||||
|
<button
|
||||||
|
className={styles['score-button']}
|
||||||
|
disabled={isCompleted}
|
||||||
|
onClick={() => handleScoreUpdate(idx+1, 1)}
|
||||||
|
aria-label={`Punkt hinzufügen für ${name}`}
|
||||||
|
>+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles['game-detail-controls']}>
|
<div className={styles['game-detail-controls']}>
|
||||||
<button className="btn" onClick={onBack} aria-label="Zurück zur Liste">Zurück zur Liste</button>
|
<button className="btn" onClick={onBack} aria-label="Zurück zur Liste">Zurück zur Liste</button>
|
||||||
|
{onUndo && (
|
||||||
|
<button
|
||||||
|
className="btn btn--secondary"
|
||||||
|
onClick={() => {
|
||||||
|
onUndo();
|
||||||
|
showToast('Letzte Aktion rückgängig gemacht', 'info');
|
||||||
|
}}
|
||||||
|
aria-label="Rückgängig"
|
||||||
|
>
|
||||||
|
↶ Rückgängig
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button className="btn" disabled={isCompleted} onClick={onFinishGame} aria-label={isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}>{isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}</button>
|
<button className="btn" disabled={isCompleted} onClick={onFinishGame} aria-label={isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}>{isCompleted ? 'Abgeschlossen' : 'Spiel beenden'}</button>
|
||||||
</div>
|
</div>
|
||||||
|
<Toast
|
||||||
|
show={toast.show}
|
||||||
|
message={toast.message}
|
||||||
|
type={toast.type}
|
||||||
|
onClose={() => setToast({ show: false, message: '', type: 'info' })}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -44,12 +44,20 @@
|
|||||||
.player-score {
|
.player-score {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 20px;
|
padding: 30px 20px;
|
||||||
border-radius: 16px;
|
border-radius: 20px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
margin: 0 8px;
|
margin: 0 8px;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-score:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: 0 12px 40px rgba(0,0,0,0.3);
|
||||||
}
|
}
|
||||||
.player-score:first-child {
|
.player-score:first-child {
|
||||||
background-color: #43a047;
|
background-color: #43a047;
|
||||||
@@ -61,21 +69,69 @@
|
|||||||
background-color: #333;
|
background-color: #333;
|
||||||
}
|
}
|
||||||
.player-name {
|
.player-name {
|
||||||
font-size: 24px;
|
font-size: 28px;
|
||||||
margin-bottom: 10px;
|
font-weight: 700;
|
||||||
|
margin-bottom: 15px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
text-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar {
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
background: rgba(255,255,255,0.2);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 10px 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, #fff 0%, #f0f0f0 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: width 0.5s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-status {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-status.active {
|
||||||
|
background: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-status.completed {
|
||||||
|
background: #ff9800;
|
||||||
|
color: #222;
|
||||||
}
|
}
|
||||||
.score {
|
.score {
|
||||||
font-size: 16vh;
|
font-size: 20vh;
|
||||||
font-weight: bold;
|
font-weight: 900;
|
||||||
margin: 10px 0 20px 0;
|
margin: 20px 0 30px 0;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-shadow: 0 2px 8px rgba(0,0,0,0.4);
|
text-shadow: 0 4px 16px rgba(0,0,0,0.6);
|
||||||
|
text-align: center;
|
||||||
|
display: block;
|
||||||
|
min-height: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.score-buttons {
|
.score-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -84,16 +140,42 @@
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
}
|
}
|
||||||
.score-button {
|
.score-button {
|
||||||
background-color: #333;
|
background: linear-gradient(135deg, #ff9800 0%, #ffa726 100%);
|
||||||
color: white;
|
color: #222;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 5px;
|
border-radius: 50%;
|
||||||
padding: 10px 20px;
|
padding: 0;
|
||||||
font-size: 18px;
|
font-size: 2.5rem;
|
||||||
|
font-weight: 900;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: all 0.3s ease;
|
||||||
min-width: 80px;
|
width: 80px;
|
||||||
margin-bottom: 8px;
|
height: 80px;
|
||||||
|
margin: 0 8px;
|
||||||
|
box-shadow: 0 4px 16px rgba(255, 152, 0, 0.3);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
touch-action: manipulation;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-button:hover:not(:disabled) {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(255, 152, 0, 0.4);
|
||||||
|
background: linear-gradient(135deg, #ffa726 0%, #ffb74d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-button:active:not(:disabled) {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-button:disabled {
|
||||||
|
background: #666;
|
||||||
|
color: #999;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
.game-controls {
|
.game-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,241 +0,0 @@
|
|||||||
import { h } from 'preact';
|
|
||||||
import { useState } from 'preact/hooks';
|
|
||||||
import styles from './GameDetail.module.css';
|
|
||||||
import modalStyles from './PlayerSelectModal.module.css';
|
|
||||||
|
|
||||||
const StartingPlayerModal = ({ players, onSelect, onCancel }) => (
|
|
||||||
<div className={modalStyles.modalOverlay}>
|
|
||||||
<div className={modalStyles.modalContent} onClick={e => e.stopPropagation()}>
|
|
||||||
<div className={modalStyles.modalHeader}>
|
|
||||||
<h3>Welcher Spieler fängt an?</h3>
|
|
||||||
{/* A cancel button isn't strictly needed if a choice is mandatory */}
|
|
||||||
</div>
|
|
||||||
<div className={modalStyles.playerList}>
|
|
||||||
{players.map((player, index) => (
|
|
||||||
<button key={player.name} className={modalStyles.playerItem} onClick={() => onSelect(index)}>
|
|
||||||
{player.name}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const GameLogTable = ({ log, players }) => {
|
|
||||||
if (!log || log.length === 0) return null;
|
|
||||||
// Only turn and foul entries
|
|
||||||
const turnEntries = log.filter(e => e.type === 'turn');
|
|
||||||
const foulEntries = log.filter(e => e.type === 'foul');
|
|
||||||
// Group into rounds (Aufnahmen): each round = one turn per player, in order
|
|
||||||
const rounds = [];
|
|
||||||
for (let i = 0; i < turnEntries.length; i += players.length) {
|
|
||||||
rounds.push(turnEntries.slice(i, i + players.length));
|
|
||||||
}
|
|
||||||
// Helper: for each player/round, sum fouls between previous and current turn
|
|
||||||
function getFoulSum(playerName, turnIdx, turnEntry) {
|
|
||||||
// Find previous turn index for this player
|
|
||||||
let prevTurnIdx = -1;
|
|
||||||
for (let i = turnIdx - 1; i >= 0; --i) {
|
|
||||||
if (turnEntries[i].player === playerName) {
|
|
||||||
prevTurnIdx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Find fouls for this player between prevTurnIdx and turnIdx
|
|
||||||
let fouls = foulEntries.filter(f => {
|
|
||||||
// Find index of this foul in log
|
|
||||||
const foulLogIdx = log.indexOf(f);
|
|
||||||
// Find log index of previous turn and current turn
|
|
||||||
const prevTurnLogIdx = prevTurnIdx >= 0 ? log.indexOf(turnEntries[prevTurnIdx]) : -1;
|
|
||||||
const currTurnLogIdx = log.indexOf(turnEntry);
|
|
||||||
return f.player === playerName && foulLogIdx > prevTurnLogIdx && foulLogIdx < currTurnLogIdx;
|
|
||||||
});
|
|
||||||
// Sum totalDeduction for all fouls
|
|
||||||
return fouls.reduce((sum, f) => sum + (f.totalDeduction || 0), 0);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className={styles['game-log-table-container']}>
|
|
||||||
<h4 className={styles['log-title']}>Aufnahmen</h4>
|
|
||||||
<table className={styles['game-log-table']}>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Aufnahme</th>
|
|
||||||
{players.map((p, idx) => (
|
|
||||||
<th key={p.name} colSpan={3} className={styles['log-player-col']}>{p.name}</th>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
{players.map((p, idx) => [
|
|
||||||
<th key={p.name + '-balls'}>Bälle</th>,
|
|
||||||
<th key={p.name + '-foul'}>Foul</th>,
|
|
||||||
<th key={p.name + '-score'}>Score</th>
|
|
||||||
])}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{rounds.map((round, roundIdx) => (
|
|
||||||
<tr key={roundIdx}>
|
|
||||||
<td>{roundIdx + 1}</td>
|
|
||||||
{players.map((p, idx) => {
|
|
||||||
const entry = round.find(e => e.player === p.name);
|
|
||||||
if (entry) {
|
|
||||||
const turnIdx = turnEntries.indexOf(entry);
|
|
||||||
const foulSum = getFoulSum(p.name, turnIdx, entry);
|
|
||||||
return [
|
|
||||||
<td key={p.name + '-balls'}>{entry.ballsPotted}</td>,
|
|
||||||
<td key={p.name + '-foul'}>{foulSum > 0 ? foulSum : ''}</td>,
|
|
||||||
<td key={p.name + '-score'}>{entry.newScore}</td>
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
return [<td key={p.name + '-balls'}></td>,<td key={p.name + '-foul'}></td>,<td key={p.name + '-score'}></td>];
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const GameDetail141 = ({ game, onUpdate, onUndo, onForfeit, onBack }) => {
|
|
||||||
const [pendingBallsLeft, setPendingBallsLeft] = useState(null); // null means not set
|
|
||||||
const [pendingFouls, setPendingFouls] = useState(0);
|
|
||||||
const [pendingReRack, setPendingReRack] = useState(0);
|
|
||||||
|
|
||||||
const handleSelectStartingPlayer = (playerIndex) => {
|
|
||||||
onUpdate({
|
|
||||||
...game,
|
|
||||||
currentPlayer: playerIndex,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// If no player is selected yet, show the modal
|
|
||||||
if (game.currentPlayer === null || game.currentPlayer === undefined) {
|
|
||||||
return <StartingPlayerModal players={game.players} onSelect={handleSelectStartingPlayer} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPlayer = game.players[game.currentPlayer];
|
|
||||||
|
|
||||||
// Handlers now only update local state
|
|
||||||
const handleBallsLeft = (num) => {
|
|
||||||
setPendingBallsLeft(num);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Foul: each press adds 1 foul point
|
|
||||||
const handleFoul = () => {
|
|
||||||
setPendingFouls(pendingFouls + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-rack: accumulate total balls added
|
|
||||||
const handleReRack = (ballsToAdd) => {
|
|
||||||
setPendingReRack(pendingReRack + ballsToAdd);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Turn change button handler
|
|
||||||
const handleTurnChange = () => {
|
|
||||||
const ballsOnTableBefore = game.ballsOnTable;
|
|
||||||
const ballsLeft = pendingBallsLeft !== null ? pendingBallsLeft : ballsOnTableBefore;
|
|
||||||
const ballsPotted = ballsOnTableBefore - ballsLeft;
|
|
||||||
let newScore = currentPlayer.score + ballsPotted - pendingFouls;
|
|
||||||
// Re-rack logic: if any re-rack, add correct points and set balls on table to 15
|
|
||||||
if (pendingReRack > 0) {
|
|
||||||
const scoreIncrement = ballsLeft + pendingReRack - 15;
|
|
||||||
newScore += scoreIncrement;
|
|
||||||
}
|
|
||||||
const updatedPlayers = game.players.map((p, idx) =>
|
|
||||||
idx === game.currentPlayer ? { ...p, score: newScore, consecutiveFouls: pendingFouls > 0 ? 0 : (p.consecutiveFouls || 0) } : p
|
|
||||||
);
|
|
||||||
let newBallsOnTable = ballsLeft;
|
|
||||||
if (pendingReRack > 0) newBallsOnTable = 15;
|
|
||||||
const newLog = [...(game.log || []), {
|
|
||||||
type: 'turn',
|
|
||||||
player: currentPlayer.name,
|
|
||||||
ballsPotted,
|
|
||||||
foulPoints: pendingFouls,
|
|
||||||
newScore,
|
|
||||||
ballsOnTable: newBallsOnTable,
|
|
||||||
reRack: pendingReRack > 0 ? pendingReRack : undefined
|
|
||||||
}];
|
|
||||||
const nextPlayer = (game.currentPlayer + 1) % game.players.length;
|
|
||||||
onUpdate({
|
|
||||||
...game,
|
|
||||||
players: updatedPlayers,
|
|
||||||
ballsOnTable: newBallsOnTable,
|
|
||||||
currentPlayer: nextPlayer,
|
|
||||||
log: newLog,
|
|
||||||
});
|
|
||||||
setPendingBallsLeft(null);
|
|
||||||
setPendingFouls(0);
|
|
||||||
setPendingReRack(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={styles['game-detail']}>
|
|
||||||
<div className={styles['game-title']}>
|
|
||||||
14/1 endlos | Race to {game.raceTo}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles['scores-container']}>
|
|
||||||
{game.players.map((p, idx) => (
|
|
||||||
<div
|
|
||||||
className={`${styles['player-score']} ${idx === game.currentPlayer ? styles['active-player'] : ''}`}
|
|
||||||
key={p.name}
|
|
||||||
>
|
|
||||||
<span className={styles['player-name']}>{p.name}</span>
|
|
||||||
<span className={styles['score']}>{p.score}</span>
|
|
||||||
{p.consecutiveFouls > 0 && (
|
|
||||||
<span className={`${styles['foul-indicator']} ${p.consecutiveFouls === 2 ? styles['foul-warning'] : ''}`}>
|
|
||||||
Fouls: {p.consecutiveFouls}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles['turn-indicator']}>
|
|
||||||
Aktueller Spieler: <strong>{currentPlayer.name}</strong> ({game.ballsOnTable} Bälle auf dem Tisch)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles['potted-balls-container']}>
|
|
||||||
<p className={styles['potted-balls-header']}>Bälle am Ende der Aufnahme:</p>
|
|
||||||
<div className={styles['potted-balls-grid']}>
|
|
||||||
{Array.from({ length: 15 }, (_, i) => i + 1).map(num => (
|
|
||||||
<button
|
|
||||||
key={num}
|
|
||||||
onClick={() => handleBallsLeft(num)}
|
|
||||||
disabled={num > game.ballsOnTable}
|
|
||||||
className={styles['potted-ball-btn'] + (pendingBallsLeft === num ? ' ' + styles['selected'] : '')}
|
|
||||||
>
|
|
||||||
{num}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles['rerack-controls']}>
|
|
||||||
<button onClick={() => handleReRack(14)} className={styles['rerack-btn'] + (pendingReRack === 14 ? ' ' + styles['selected'] : '')}>+14 Re-Rack</button>
|
|
||||||
<button onClick={() => handleReRack(15)} className={styles['rerack-btn'] + (pendingReRack === 15 ? ' ' + styles['selected'] : '')}>+15 Re-Rack</button>
|
|
||||||
<button onClick={handleFoul} className={styles['rerack-btn'] + (pendingFouls > 0 ? ' ' + styles['selected'] : '')}>Foul -1</button>
|
|
||||||
{pendingFouls > 0 && <span className={styles['pending-foul-info']}>Foulpunkte: {pendingFouls}</span>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles['turn-change-controls']}>
|
|
||||||
<button className={styles['turn-change-btn']} onClick={handleTurnChange}>
|
|
||||||
⇄
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<GameLogTable log={game.log} players={game.players} />
|
|
||||||
|
|
||||||
<div className={styles['game-detail-controls']}>
|
|
||||||
<button className="btn" onClick={onUndo} disabled={!game.undoStack || game.undoStack.length === 0}>Undo</button>
|
|
||||||
<button className="btn btn-danger" onClick={onForfeit}>Forfeit</button>
|
|
||||||
<button className="btn" onClick={onBack}>Zurück zur Liste</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GameDetail141;
|
|
||||||
@@ -479,7 +479,7 @@ const Player3Step = ({ playerNameHistory, onNext, onCancel, initialValue = '' })
|
|||||||
*/
|
*/
|
||||||
const GameTypeStep = ({ onNext, onCancel, initialValue = '' }) => {
|
const GameTypeStep = ({ onNext, onCancel, initialValue = '' }) => {
|
||||||
const [gameType, setGameType] = useState(initialValue);
|
const [gameType, setGameType] = useState(initialValue);
|
||||||
const gameTypes = ['8-Ball', '9-Ball', '10-Ball', '14/1 endlos'];
|
const gameTypes = ['8-Ball', '9-Ball', '10-Ball'];
|
||||||
|
|
||||||
const handleSelect = (selectedType) => {
|
const handleSelect = (selectedType) => {
|
||||||
setGameType(selectedType);
|
setGameType(selectedType);
|
||||||
@@ -561,14 +561,8 @@ const GameTypeStep = ({ onNext, onCancel, initialValue = '' }) => {
|
|||||||
* @returns {import('preact').VNode}
|
* @returns {import('preact').VNode}
|
||||||
*/
|
*/
|
||||||
const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }) => {
|
const RaceToStep = ({ onNext, onCancel, initialValue = '', gameType }) => {
|
||||||
let quickPicks, defaultValue;
|
const quickPicks = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
if (gameType === '14/1 endlos') {
|
const defaultValue = 5;
|
||||||
quickPicks = [60, 70, 80, 90, 100];
|
|
||||||
defaultValue = 80;
|
|
||||||
} else {
|
|
||||||
quickPicks = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
||||||
defaultValue = 5;
|
|
||||||
}
|
|
||||||
const [raceTo, setRaceTo] = useState(initialValue !== '' ? initialValue : defaultValue);
|
const [raceTo, setRaceTo] = useState(initialValue !== '' ? initialValue : defaultValue);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
56
src/components/Toast.jsx
Normal file
56
src/components/Toast.jsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { h } from 'preact';
|
||||||
|
import { useEffect, useState } from 'preact/hooks';
|
||||||
|
import styles from './Toast.module.css';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toast notification component for user feedback
|
||||||
|
* @param {object} props
|
||||||
|
* @param {boolean} props.show
|
||||||
|
* @param {string} props.message
|
||||||
|
* @param {string} props.type - 'success', 'error', 'info'
|
||||||
|
* @param {Function} props.onClose
|
||||||
|
* @param {number} [props.duration=3000]
|
||||||
|
* @returns {import('preact').VNode|null}
|
||||||
|
*/
|
||||||
|
const Toast = ({ show, message, type = 'info', onClose, duration = 3000 }) => {
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (show) {
|
||||||
|
setIsVisible(true);
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 300); // Wait for animation to complete
|
||||||
|
}, duration);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [show, duration, onClose]);
|
||||||
|
|
||||||
|
if (!show && !isVisible) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`${styles.toast} ${styles[type]} ${isVisible ? styles.show : styles.hide}`}>
|
||||||
|
<div className={styles.toastContent}>
|
||||||
|
<div className={styles.toastIcon}>
|
||||||
|
{type === 'success' && '✓'}
|
||||||
|
{type === 'error' && '✕'}
|
||||||
|
{type === 'info' && 'ℹ'}
|
||||||
|
</div>
|
||||||
|
<span className={styles.toastMessage}>{message}</span>
|
||||||
|
<button
|
||||||
|
className={styles.toastClose}
|
||||||
|
onClick={() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
setTimeout(onClose, 300);
|
||||||
|
}}
|
||||||
|
aria-label="Schließen"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Toast;
|
||||||
|
|
||||||
134
src/components/Toast.module.css
Normal file
134
src/components/Toast.module.css
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 10000;
|
||||||
|
min-width: 300px;
|
||||||
|
max-width: 400px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.hide {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastContent {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastIcon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastMessage {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastClose {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 18px;
|
||||||
|
color: #666;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toastClose:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast types */
|
||||||
|
.toast.success {
|
||||||
|
border-left: 4px solid #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success .toastIcon {
|
||||||
|
background: #4caf50;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error .toastIcon {
|
||||||
|
background: #f44336;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
border-left: 4px solid #2196f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info .toastIcon {
|
||||||
|
background: #2196f3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animation keyframes */
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideOutRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
animation: slideInRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.hide {
|
||||||
|
animation: slideOutRight 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export default function NewGameScreen({
|
|||||||
const handleGameTypeNext = (type: string) => {
|
const handleGameTypeNext = (type: string) => {
|
||||||
onDataChange({
|
onDataChange({
|
||||||
gameType: type as any, // Type assertion for now, could be improved with proper validation
|
gameType: type as any, // Type assertion for now, could be improved with proper validation
|
||||||
raceTo: type === '14/1 endlos' ? '150' : '50'
|
raceTo: '8'
|
||||||
});
|
});
|
||||||
onStepChange('raceTo');
|
onStepChange('raceTo');
|
||||||
};
|
};
|
||||||
|
|||||||
70
src/layouts/BaseLayout.astro
Normal file
70
src/layouts/BaseLayout.astro
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
---
|
||||||
|
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="viewport" content="interactive-widget=resizes-content">
|
||||||
|
<title>BSC Score</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.layout-header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 10vh;
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.layout-footer {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 10vh;
|
||||||
|
background: #222;
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
.layout-main {
|
||||||
|
min-height: 80vh;
|
||||||
|
margin-top: 10vh;
|
||||||
|
margin-bottom: 10vh;
|
||||||
|
width: 100vw;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="layout-header">
|
||||||
|
<!-- BUTTONS PLACEHOLDER -->
|
||||||
|
<div style="width:100%; display:flex; justify-content:center; gap:1rem;">
|
||||||
|
<button disabled>Button 1</button>
|
||||||
|
<button disabled>Button 2</button>
|
||||||
|
<button disabled>Button 3</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<main class="layout-main">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<footer class="layout-footer">
|
||||||
|
<!-- INFO PLACEHOLDER -->
|
||||||
|
<div style="width:100%; text-align:center;">
|
||||||
|
<span>Informational text goes here. © {new Date().getFullYear()} BSC Score</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -42,38 +42,20 @@ export class GameService {
|
|||||||
undoStack: [],
|
undoStack: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (gameData.gameType === '14/1 endlos') {
|
const standardGame: StandardGame = {
|
||||||
const players = [
|
...baseGame,
|
||||||
{ name: gameData.player1, score: 0, consecutiveFouls: 0 },
|
player1: gameData.player1,
|
||||||
{ name: gameData.player2, score: 0, consecutiveFouls: 0 }
|
player2: gameData.player2,
|
||||||
];
|
score1: 0,
|
||||||
|
score2: 0,
|
||||||
if (gameData.player3) {
|
};
|
||||||
players.push({ name: gameData.player3, score: 0, consecutiveFouls: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
if (gameData.player3) {
|
||||||
...baseGame,
|
standardGame.player3 = gameData.player3;
|
||||||
players,
|
standardGame.score3 = 0;
|
||||||
currentPlayer: null,
|
|
||||||
ballsOnTable: 15,
|
|
||||||
} as EndlosGame;
|
|
||||||
} else {
|
|
||||||
const standardGame: StandardGame = {
|
|
||||||
...baseGame,
|
|
||||||
player1: gameData.player1,
|
|
||||||
player2: gameData.player2,
|
|
||||||
score1: 0,
|
|
||||||
score2: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (gameData.player3) {
|
|
||||||
standardGame.player3 = gameData.player3;
|
|
||||||
standardGame.score3 = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return standardGame;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return standardGame;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -149,6 +149,20 @@ input:focus, select:focus {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn--secondary {
|
||||||
|
background: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 2px solid #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--secondary:hover {
|
||||||
|
background: #444;
|
||||||
|
border-color: #666;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
background: var(--color-primary-hover);
|
background: var(--color-primary-hover);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export type GameStatus = 'active' | 'completed';
|
export type GameStatus = 'active' | 'completed';
|
||||||
|
|
||||||
export type GameType = '8-Ball' | '9-Ball' | '10-Ball' | '14/1 endlos';
|
export type GameType = '8-Ball' | '9-Ball' | '10-Ball';
|
||||||
|
|
||||||
export interface Player {
|
export interface Player {
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export const GAME_TYPES: Array<{ value: GameType; label: string; defaultRaceTo:
|
|||||||
{ value: '8-Ball', label: '8-Ball', defaultRaceTo: 5 },
|
{ value: '8-Ball', label: '8-Ball', defaultRaceTo: 5 },
|
||||||
{ value: '9-Ball', label: '9-Ball', defaultRaceTo: 9 },
|
{ value: '9-Ball', label: '9-Ball', defaultRaceTo: 9 },
|
||||||
{ value: '10-Ball', label: '10-Ball', defaultRaceTo: 10 },
|
{ value: '10-Ball', label: '10-Ball', defaultRaceTo: 10 },
|
||||||
{ value: '14/1 endlos', label: '14/1 Endlos', defaultRaceTo: 150 },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RACE_TO_OPTIONS = [
|
export const RACE_TO_OPTIONS = [
|
||||||
|
|||||||
Reference in New Issue
Block a user