Skip to content

Commit

Permalink
Add table login
Browse files Browse the repository at this point in the history
  • Loading branch information
sbutz committed Nov 26, 2023
1 parent 20a4a8d commit 467f9f6
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 143 deletions.
11 changes: 7 additions & 4 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { CssBaseline, ThemeProvider, createTheme } from '@mui/material';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { LocalizationProvider } from '@mui/x-date-pickers';

import { Store } from './store/Store';
import Router from './Router';
import AuthProvider from './store/AuthProvider';
import ClubProvider from './store/ClubProvider';
import TableProvider from './store/TableProvider';
import { Store } from './store/Store';

const darkTheme = createTheme({
palette: {
Expand All @@ -19,9 +20,11 @@ export default function App() {
<LocalizationProvider dateAdapter={AdapterDayjs}>
<AuthProvider>
<ClubProvider>
<Store>
<Router />
</Store>
<TableProvider>
<Store>
<Router />
</Store>
</TableProvider>
</ClubProvider>
</AuthProvider>
<CssBaseline />
Expand Down
50 changes: 26 additions & 24 deletions client/src/components/AppDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from '@mui/material';
import {
CalendarMonth,
Close, Home, Logout, TableRestaurant,
Close, Home, Login, Logout, TableRestaurant,
} from '@mui/icons-material';
import { useAuth } from '../store/AuthProvider';
import { footerText } from './Footer';
Expand All @@ -22,8 +22,11 @@ const drawerBoxSx = {

export default memo((props: AppDrawerProps) => {
const theme = useTheme();
const { userId, userIdLoading, signOut } = useAuth();
const {
userId, userIdLoading, signOut, admin,
} = useAuth();
const isLoggedIn = userId && !userIdLoading;
const isAdmin = isLoggedIn && admin;

const drawerList = (
<List>
Expand All @@ -43,18 +46,7 @@ export default memo((props: AppDrawerProps) => {
</ListItemButton>
</ListItem>
<Divider />
{ /* isLoggedIn ? null
: (
<ListItem>
<ListItemButton component={Link} to="/login">
<ListItemIcon>
<Login />
</ListItemIcon>
<ListItemText primary="Login" />
</ListItemButton>
</ListItem>
) */}
{ isLoggedIn
{ isAdmin
? (
<>
<ListItem>
Expand All @@ -74,17 +66,27 @@ export default memo((props: AppDrawerProps) => {
</ListItemButton>
</ListItem>
<Divider />
<ListItem>
<ListItemButton onClick={signOut}>
<ListItemIcon>
<Logout />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItemButton>
</ListItem>
</>
)
: null}
) : null }
{ isLoggedIn ? (
<ListItem>
<ListItemButton onClick={signOut}>
<ListItemIcon>
<Logout />
</ListItemIcon>
<ListItemText primary="Logout" />
</ListItemButton>
</ListItem>
) : (
<ListItem>
<ListItemButton component={Link} to="/login">
<ListItemIcon>
<Login />
</ListItemIcon>
<ListItemText primary="Login" />
</ListItemButton>
</ListItem>
)}
<ListItem disablePadding sx={{ position: 'fixed', bottom: 0 }}>
<ListItemButton component={Link} to="/legal">
<ListItemText primary={footerText} secondary="Legal" />
Expand Down
18 changes: 11 additions & 7 deletions client/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ import { useAuth } from '../store/AuthProvider';
import AppDrawer from './AppDrawer';

export default function Header() {
const { userId, userIdLoading, signOut } = useAuth();
const loggedIn = userId && !userIdLoading;
const {
userId, userIdLoading, signOut, admin,
} = useAuth();
const isLoggedIn = userId && !userIdLoading;
const isAdmin = isLoggedIn && admin;

const [open, setOpen] = useState(false);
const closeDrawer = useCallback(() => { setOpen(false); }, []);
Expand Down Expand Up @@ -39,14 +42,15 @@ export default function Header() {
<>
<Button component={Link} to="/home">Poolscore</Button>
<Box sx={{ flexGrow: 1 }} />
{ loggedIn ? (
{ isAdmin ? (
<>
<Button component={Link} to="/matchdays">Spieltage</Button>
<Button component={Link} to="/matchday">Spieltage</Button>
<Button component={Link} to="/tables">Tische</Button>
<Button onClick={signOut}>Logout</Button>
</>
)
: null /* <Button component={Link} to="/login">Login</Button> */}
) : null }
{ isLoggedIn
? <Button onClick={signOut}>Logout</Button>
: <Button component={Link} to="/login">Login</Button> }
</>
);

Expand Down
8 changes: 6 additions & 2 deletions client/src/components/HomeLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { Stack, useMediaQuery, useTheme } from '@mui/material';
import {
Container, Stack, useMediaQuery, useTheme,
} from '@mui/material';
import Footer from './Footer';
import Header from './Header';

Expand All @@ -13,7 +15,9 @@ export default function Layout({ children }: LayoutProps) {
return (
<Stack height="100vh" spacing={5} alignItems="center">
<Header />
{children}
<Container>
{children}
</Container>
{isDesktop ? <Footer /> : null}
</Stack>
);
Expand Down
33 changes: 24 additions & 9 deletions client/src/store/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@ import {
ReactNode, createContext, useEffect, useContext, useMemo, useCallback,
} from 'react';
import { AuthError } from 'firebase/auth';
import { doc } from 'firebase/firestore';
import {
useAuthState, useCreateUserWithEmailAndPassword, useSignInWithEmailAndPassword, useSignOut,
} from 'react-firebase-hooks/auth';
import { useDocumentData } from 'react-firebase-hooks/firestore';

import { auth, db } from './Firebase';
import { auth } from './Firebase';
import useIdTokenResult from '../util/useIdTokenResult';
import useSignInWithTableToken from '../util/useSignInWithTableToken';

interface AuthState {
userId?: string;
userIdLoading: boolean;
clubId?: string;
admin?: boolean;
signUp: (email: string, password: string) => void;
signUpError?: AuthError;
signIn: (email: string, password: string) => void;
signInError?: AuthError;
signInWithToken: (token: string) => void;
signInWithTokenError?: AuthError;
signOut: () => void;
signOutError?: AuthError;
}
Expand All @@ -36,10 +39,7 @@ interface AuthProviderProps {
export default function AuthProvider({ children }: AuthProviderProps) {
const [user, userLoading, errorAuth] = useAuthState(auth, {});
useEffect(() => { if (errorAuth) throw errorAuth; }, [errorAuth]);

const userRef = user?.uid ? doc(db, 'users', user.uid) : null;
const [userData, , userDataError] = useDocumentData(userRef);
useEffect(() => { if (userDataError) throw userDataError; }, [userDataError]);
const tokenResult = useIdTokenResult(user);

const [firebaseSignUp, , , signUpError] = useCreateUserWithEmailAndPassword(auth);
const signUp = useCallback((email: string, password: string) => {
Expand All @@ -53,6 +53,8 @@ export default function AuthProvider({ children }: AuthProviderProps) {
firebaseSignIn(email, password);
}, [user, firebaseSignIn]);

const [signInWithToken, , , signInWithTokenError] = useSignInWithTableToken(auth);

const [firebaseSignOut, , signOutError] = useSignOut(auth);
const signOut = useCallback(() => {
if (!user) throw Error('To sign out you need to sign in first.');
Expand All @@ -62,15 +64,28 @@ export default function AuthProvider({ children }: AuthProviderProps) {
const value = useMemo(() => ({
userId: user?.uid,
userIdLoading: userLoading,
clubId: userData?.club.id,
clubId: tokenResult?.claims.clubId,
admin: tokenResult?.claims.admin,
signUp,
signUpError,
signIn,
signInError,
signInWithToken,
signInWithTokenError,
signOut,
signOutError: signOutError as AuthError,
}), [
user, userLoading, userData, signUp, signUpError, signIn, signInError, signOut, signOutError,
user,
userLoading,
tokenResult,
signUp,
signUpError,
signIn,
signInError,
signInWithToken,
signInWithTokenError,
signOut,
signOutError,
]);

return (
Expand Down
2 changes: 1 addition & 1 deletion client/src/store/ClubProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function ClubProvider({ children }: ClubProviderProps) {
useEffect(() => { if (clubDataError) throw clubDataError; }, [clubDataError]);

const setName = useCallback(async (name: string) => {
if (!clubId) throw Error("Club's id no given. Cannot update it's name.");
if (!clubId) throw Error("Club's id not given. Cannot update it's name.");

await updateDoc(doc(db, 'clubs', clubId), { name });
}, [clubId]);
Expand Down
2 changes: 1 addition & 1 deletion client/src/store/Firebase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ if (isDevelopment()) { connectFirestoreEmulator(db, 'localhost', 8080); }
const functions = getFunctions(app);
if (isDevelopment()) { connectFunctionsEmulator(functions, 'localhost', 5001); }

export { auth, db };
export { auth, db, functions };
30 changes: 2 additions & 28 deletions client/src/store/Store.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, {
useEffect, createContext, Dispatch, useReducer, useMemo, useCallback,
createContext, Dispatch, useReducer, useMemo, useCallback,
} from 'react';
import {
doc, collection, onSnapshot, addDoc, deleteDoc,
doc, collection, addDoc, deleteDoc,
} from 'firebase/firestore';

import { db } from './Firebase';
Expand Down Expand Up @@ -129,32 +129,6 @@ export function Store({ children } : StoreProps) {

const value = useMemo(() => [state, asyncDispatch] as ContextType, [state, asyncDispatch]);

useEffect(() => {
const clubRef = doc(db, 'club', state.id);
return onSnapshot(clubRef, (snapshot) => {
if (snapshot.exists()) {
dispatch({
type: 'set_name',
name: snapshot.data().name,
});
}
});
}, [state.id]);

useEffect(() => {
const tableRef = collection(db, 'club', state.id, 'tables');
return onSnapshot(tableRef, (snapshot) => {
const tmp = [] as Pooltable[];
snapshot.forEach((d) => {
tmp.push({ id: d.id, name: d.data().name });
});
dispatch({
type: 'set_tables',
tables: tmp.sort((a, b) => a.name.localeCompare(b.name)),
});
});
}, [state.id]);

return (
<Context.Provider value={value}>
{children}
Expand Down
78 changes: 78 additions & 0 deletions client/src/store/TableProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import {
createContext, ReactNode, useCallback, useContext, useMemo,
} from 'react';
import {
collection, deleteDoc, doc, setDoc,
} from 'firebase/firestore';
import { useCollection } from 'react-firebase-hooks/firestore';
import { httpsCallable } from 'firebase/functions';
import { useAuth } from './AuthProvider';
import { db, functions } from './Firebase';

interface Token {
value: string;
expires: Date;
}

interface Table {
name: string;
token?: Token;
}

interface TableState {
tables: Table[];
addTable: (name: string) => Promise<void>;
removeTable: (name: string) => Promise<void>;
generateToken: (name: string) => Promise<void>;
}

const TableContext = createContext<TableState | undefined>(undefined);

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

interface TableProviderProps {
children: ReactNode;
}
export default function TableProvider({ children }: TableProviderProps) {
const { clubId } = useAuth();

const tablesRef = clubId ? collection(db, 'clubs', clubId, 'tables') : null;
const [values] = useCollection(tablesRef);
// useEffect(() => { if (error) throw error; }, [error]);

const addTable = useCallback(async (name: string) => {
if (!clubId) throw Error("Club's id not given. Cannot add a table.");

const ref = doc(db, 'clubs', clubId, 'tables', name);
await setDoc(ref, {});
}, [clubId]);

const removeTable = useCallback(async (name: string) => {
if (!clubId) throw Error("Club's id not given. Cannot remove a table.");

const ref = doc(db, 'clubs', clubId, 'tables', name);
await deleteDoc(ref);
}, [clubId]);

const generateToken = useCallback(async (name: string) => {
const createToken = httpsCallable(functions, 'table-createtoken');
await createToken({ tableId: name });
}, []);

const value = useMemo(() => ({
tables: values ? values.docs.map((d) => ({ name: d.id, ...d.data() })) : [],
addTable,
removeTable,
generateToken,
}), [values, addTable, removeTable, generateToken]);

return (
<TableContext.Provider value={value}>
{children}
</TableContext.Provider>
);
}
7 changes: 7 additions & 0 deletions client/src/util/Validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,15 @@ export function AuthValidator(error?: AuthError): Validator {
return 'Es ist kein Nutzer mit dieser E-Mail-Adresse registriert.';
case 'auth/wrong-password':
return 'Das Passwort ist falsch.';
case 'functions/not-found':
return 'Ungültiger oder abgelaufener Tischcode';
default:
throw error;
}
};
}

export function TableCodeValidator(value: string) {
// eslint-disable-next-line react/destructuring-assignment
return value.trim().match('[0-9A-Z]{4}') ? null : 'Ungültiger Tischcode.';
}
Loading

0 comments on commit 467f9f6

Please sign in to comment.