Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync Table Score #53

Merged
merged 9 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Router from './Router';
import AuthProvider from './store/AuthProvider';
import ClubProvider from './store/ClubProvider';
import TableProvider from './store/TableProvider';
import GameProvider from './store/GameProvider';
import { Store } from './store/Store';

const darkTheme = createTheme({
Expand All @@ -21,9 +22,11 @@ export default function App() {
<AuthProvider>
<ClubProvider>
<TableProvider>
<Store>
<Router />
</Store>
<GameProvider>
<Store>
<Router />
</Store>
</GameProvider>
</TableProvider>
</ClubProvider>
</AuthProvider>
Expand Down
2 changes: 0 additions & 2 deletions client/src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {

import ErrorPage from './views/ErrorPage';
import Game from './views/Game';
import Game14 from './views/Game14';
import Tables from './views/Tables';
import Matchdays from './views/Matchdays';
import Matchday from './views/Matchday';
Expand All @@ -21,7 +20,6 @@ export default function Router() {
<Route path="/" element={<Navigate to="/game" replace />} />
<Route path="/home" element={<Homepage />} />
<Route path="/game" element={<Game />} />
<Route path="/game14" element={<Game14 />} />
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
<Route path="/tables" element={<RequireLogin><Tables /></RequireLogin>} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@ import { Home } from '@mui/icons-material';
import {
AppBar, Toolbar, IconButton, Stack,
} from '@mui/material';
import { memo } from 'react';
import { Link } from 'react-router-dom';

interface HeaderProps {
children: React.ReactNode;
}

function Header({ children }: HeaderProps) {
export default function GameFooter({ children }: HeaderProps) {
return (
<AppBar color="transparent" position="sticky">
<Toolbar>
Expand All @@ -28,5 +27,3 @@ function Header({ children }: HeaderProps) {
</AppBar>
);
}

export default memo(Header);
2 changes: 1 addition & 1 deletion client/src/components/GameLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Stack, Container } from '@mui/material';

import RequireDesktop from './RequireDesktop';
import GameBar from './GameBar';
import GameBar from './GameFooter';

interface LayoutProps {
children: React.ReactNode;
Expand Down
42 changes: 42 additions & 0 deletions client/src/components/GameToolbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
SportsEsports, PlayCircleFilled, Undo,
} from '@mui/icons-material';
import { Button } from '@mui/material';
import { Mode } from '../store/GameModes';

interface GameToolbarProps {
mode: Mode,
onRestart: () => void;
onChangeMode: () => void;
onRollback: () => void;
}

export default function GameToolbar({
mode, onRestart, onChangeMode, onRollback,
}:GameToolbarProps) {
return (
<>
<Button
color="inherit"
startIcon={<SportsEsports />}
onClick={onChangeMode}
>
{mode === Mode.Ball8 ? '14/1 endlos' : '8/9/10 Ball'}
</Button>
<Button
color="inherit"
startIcon={<PlayCircleFilled />}
onClick={onRestart}
>
Neues Spiel
</Button>
<Button
color="inherit"
startIcon={<Undo />}
onClick={onRollback}
>
Rückgängig
</Button>
</>
);
}
2 changes: 2 additions & 0 deletions client/src/store/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ interface AuthState {
userId?: string;
userIdLoading: boolean;
clubId?: string;
tableId?: string;
admin?: boolean;
signUp: (email: string, password: string) => void;
signUpError?: AuthError;
Expand Down Expand Up @@ -66,6 +67,7 @@ export default function AuthProvider({ children }: AuthProviderProps) {
userIdLoading: userLoading,
clubId: tokenResult?.claims.clubId,
admin: tokenResult?.claims.admin,
tableId: tokenResult?.claims.tableId,
signUp,
signUpError,
signIn,
Expand Down
13 changes: 7 additions & 6 deletions client/src/store/Firebase.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { initializeApp } from 'firebase/app';
import { connectAuthEmulator, getAuth } from 'firebase/auth';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { initializeFirestore, connectFirestoreEmulator, enableMultiTabIndexedDbPersistence } from 'firebase/firestore';
import { getFunctions, connectFunctionsEmulator } from 'firebase/functions';
import isDevelopment from '../util/environment';
import { isDevelopmentEnv, isTestEnv } from '../util/environment';

const firebaseConfig = {
apiKey: 'AIzaSyD4jUVmL7dUKi7l6fQqmxHqzVZR_sidLG8',
Expand All @@ -16,12 +16,13 @@ const firebaseConfig = {
const app = initializeApp(firebaseConfig);

const auth = getAuth(app);
if (isDevelopment()) { connectAuthEmulator(auth, 'http://localhost:9099', { disableWarnings: true }); }
if (isDevelopmentEnv()) { connectAuthEmulator(auth, 'http://localhost:9099', { disableWarnings: true }); }

const db = getFirestore(app);
if (isDevelopment()) { connectFirestoreEmulator(db, 'localhost', 8080); }
const db = initializeFirestore(app, { ignoreUndefinedProperties: true });
if (isDevelopmentEnv()) { connectFirestoreEmulator(db, 'localhost', 8080); }
if (!isTestEnv()) { enableMultiTabIndexedDbPersistence(db); }

const functions = getFunctions(app);
if (isDevelopment()) { connectFunctionsEmulator(functions, 'localhost', 5001); }
if (isDevelopmentEnv()) { connectFunctionsEmulator(functions, 'localhost', 5001); }

export { auth, db, functions };
31 changes: 31 additions & 0 deletions client/src/store/GameModes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
State as State8, Action as Action8, initialState as initial8State, reducer as reducer8,
} from './GameState';
import {
State as State14, Action as Action14, initialState as initial14state, reducer as reducer14,
} from './GameState14';

export enum Mode {
Ball8 = 'BALL8',
Straight = 'STRAIGHT',
}

export type State = State8 | State14;

export type Action = Action8 | Action14;

export const getInitialState = (mode: Mode) => {
switch (mode) {
case Mode.Ball8: return initial8State;
case Mode.Straight: return initial14state;
default: throw Error(`Unkown Mode '${mode}'`);
}
};

export const reducer = (mode: Mode, state: State, action: Action) => {
switch (mode) {
case Mode.Ball8: return reducer8(state as State8, action as Action8);
case Mode.Straight: return reducer14(state as State14, action as Action14);
default: throw Error(`Unkown Mode '${mode}'`);
}
};
87 changes: 87 additions & 0 deletions client/src/store/GameProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {
createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState,
} from 'react';
import { useCollectionData } from 'react-firebase-hooks/firestore';
import {
collection, doc, query, where, updateDoc, limit,
} from 'firebase/firestore';
import { useAuth } from './AuthProvider';
import { db } from './Firebase';
import {
Mode, State, Action, getInitialState, reducer,
} from './GameModes';

interface GameState {
mode?: Mode;
state: State;
startNewGame: (mode: Mode) => void;
updateState: (action: Action) => void;
}

const GameContext = createContext<GameState | undefined>(undefined);

export function useGame() {
const value = useContext(GameContext);
if (value === undefined) throw new Error('Expected games context value to be set.');
return value;
}

interface GameProviderProps {
children: ReactNode;
}
export default function GameProvider({ children }: GameProviderProps) {
const { clubId, tableId } = useAuth();
const [mode, setMode] = useState<Mode>(Mode.Ball8);
const [state, setState] = useState<State>(getInitialState(mode));

const clubRef = clubId ? doc(db, `clubs/${clubId}`) : null;
const tableRef = clubId && tableId ? doc(db, `clubs/${clubId}/tables/${tableId}`) : null;
const q = clubRef && tableRef
? query(collection(db, 'games'), where('club', '==', clubRef), where('table', '==', tableRef), limit(1))
: null;
const [gameData, gameDataLoading, gameDataError, snapshot] = useCollectionData(q);
const gameRef = snapshot ? snapshot.docs[0].ref : null;
useEffect(() => { if (gameDataError) throw gameDataError; }, [gameDataError]);

// Sync behaviour:
// - no online state available
// - just use local state
// - start new game
// - online state available
// - local changes -> push to firestore
// - changes in firestore (e.g. from other client) -> apply to local
const onlineSync = gameRef && !gameDataError && !gameDataLoading
&& gameData?.at(0) && gameData.at(0)?.mode;
useEffect(() => {
if (onlineSync) {
setMode(gameData?.at(0)?.mode);
setState(gameData?.at(0)?.state);
}
}, [gameData, onlineSync]);

const startNewGame = useCallback(async (newMode: Mode) => {
setMode(newMode);
setState(getInitialState(newMode));
if (onlineSync) {
await updateDoc(gameRef, { mode: newMode, state: getInitialState(newMode) });
}
}, [gameRef, onlineSync]);

const updateState = useCallback(async (action: Action) => {
const newState = reducer(mode, state, action);
setState(newState);
if (onlineSync) {
await updateDoc(gameRef, { state: newState });
}
}, [mode, state, setState, gameRef, onlineSync]);

const value = useMemo(() => ({
mode, state, startNewGame, updateState,
}), [mode, state, startNewGame, updateState]);

return (
<GameContext.Provider value={value}>
{children}
</GameContext.Provider>
);
}
5 changes: 2 additions & 3 deletions client/src/store/GameState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface State {
export interface State {
actions: Action[];
scoreHome: number;
scoreGuest: number;
Expand All @@ -9,7 +9,7 @@ const initialState = {
scoreGuest: 0,
};

interface Action {
export interface Action {
type: 'HOME_PLUS_ONE'
| 'HOME_MINUS_ONE'
| 'GUEST_PLUS_ONE'
Expand All @@ -18,7 +18,6 @@ interface Action {
| 'RESET';
}

// eslint-disable-next-line consistent-return
function reducer(state: State, action: Action) : State {
const actions = [...state.actions, action];

Expand Down
4 changes: 2 additions & 2 deletions client/src/store/GameState14.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const initialPlayerState = {
fouls: 0,
};

interface State {
export interface State {
actions: Action[];
activePlayer: undefined | 'home' | 'guest';
remainingBalls: number;
Expand Down Expand Up @@ -66,7 +66,7 @@ interface ResetAction {
type: 'RESET';
}

type Action = SetActivePlayerAction
export type Action = SetActivePlayerAction
| SetRemainingBallsAction
| BreakFoulAction
| FoulAction
Expand Down
4 changes: 4 additions & 0 deletions client/src/store/Store.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* @module @deprecated This component should be removed
*/

import React, {
createContext, Dispatch, useReducer, useMemo, useCallback,
} from 'react';
Expand Down
2 changes: 2 additions & 0 deletions client/src/util/Validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export function AuthValidator(error?: AuthError): Validator {
return 'Die E-Mail-Adresse ist ungültig.';
case 'auth/invalid-password':
return 'Das Passwort muss mindestens 6 Zeichen lang sein.';
case 'auth/internal-error':
return 'Du scheinst offline zu sein. Bitte prüfe deine Internetverbindung.';
case 'auth/user-not-found':
return 'Es ist kein Nutzer mit dieser E-Mail-Adresse registriert.';
case 'auth/wrong-password':
Expand Down
5 changes: 4 additions & 1 deletion client/src/util/environment.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export default function isDevelopment() {
export function isDevelopmentEnv() {
return process.env.NODE_ENV === 'development';
}
export function isTestEnv() {
return process.env.NODE_ENV === 'test';
}
Loading
Loading