diff --git a/bun.lock b/bun.lock index d5b46a6..0fddf76 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "bun-react-template", @@ -10,7 +11,8 @@ "@mui/icons-material": "7.3.5", "@mui/material": "7.3.5", "cookie": "^1.0.2", - "mobx-react-lite": "^4.1.1", + "mobx": "^6.15.0", + "mobx-react": "^9.2.1", "react": "19", "react-dom": "19", "react-router-dom": "^7.9.5", @@ -538,6 +540,8 @@ "mobx": ["mobx@6.15.0", "", {}, "sha512-UczzB+0nnwGotYSgllfARAqWCJ5e/skuV2K/l+Zyck/H6pJIhLXuBnz+6vn2i211o7DtbE78HQtsYEKICHGI+g=="], + "mobx-react": ["mobx-react@9.2.1", "", { "dependencies": { "mobx-react-lite": "^4.1.1" }, "peerDependencies": { "mobx": "^6.9.0", "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-WJNNm0FB2n0Z0u+jS1QHmmWyV8l2WiAj8V8I/96kbUEN2YbYCoKW+hbbqKKRUBqElu0llxM7nWKehvRIkhBVJw=="], + "mobx-react-lite": ["mobx-react-lite@4.1.1", "", { "dependencies": { "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "mobx": "^6.9.0", "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-iUxiMpsvNraCKXU+yPotsOncNNmyeS2B5DKL+TL6Tar/xm+wwNJAubJmtRSeAoYawdZqwv8Z/+5nPRHeQxTiXg=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], diff --git a/data.sqlite b/data.sqlite index 976d2c5..9446832 100644 Binary files a/data.sqlite and b/data.sqlite differ diff --git a/package.json b/package.json index 1110b31..c7ebc39 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "@mui/icons-material": "7.3.5", "@mui/material": "7.3.5", "cookie": "^1.0.2", - "mobx-react-lite": "^4.1.1", + "mobx": "^6.15.0", + "mobx-react": "^9.2.1", "react": "19", "react-dom": "19", "react-router-dom": "^7.9.5" diff --git a/src/client/App.tsx b/src/client/App.tsx index 1108e0f..2db0a54 100644 --- a/src/client/App.tsx +++ b/src/client/App.tsx @@ -1,8 +1,16 @@ -import SignIn from './components/SignIn'; -import { createTheme, ThemeProvider } from '@mui/material'; -import { useStore } from './Store'; +import SignIn from './components/authentication/SignIn'; +import { createTheme, CssBaseline, ThemeProvider } from '@mui/material'; +import { useStore, type View } from './Store'; import Group from './components/Group'; import { observer } from 'mobx-react-lite'; +import { type ReactElement } from 'react'; +import NewGroup from './components/authentication/NewGroup'; + +export const views: Record = { + signIn: , + group: , + newGroup: , +}; const App = observer(() => { const theme = createTheme({ @@ -12,15 +20,18 @@ const App = observer(() => { }); const store = useStore(); - const { loggedIn } = store; + const { currentView } = store; return ( -
- {loggedIn ? - : - - } + +
+ {views[currentView]}
); diff --git a/src/client/Store.ts b/src/client/Store.ts index 5386b41..85cb1f0 100644 --- a/src/client/Store.ts +++ b/src/client/Store.ts @@ -1,36 +1,64 @@ import { createContext, useContext } from 'react'; -import { makeAutoObservable } from 'mobx'; import cookie from 'cookie'; import type { Group, User } from '@/interfaces'; +import { makeAutoObservable } from 'mobx'; export type CookieData = { user: User; group: Group; }; +export type View = 'signIn' | 'group' | 'newGroup'; + export class Store { - cookieData: CookieData | null = null; + loggedIn = false; + currentView: View = 'signIn'; + user: User | null = null; + group: Group | null = null; - processCookie() { - const parsed = cookie.parse(document.cookie); - - this.cookieData = { - user: parsed.user ? JSON.parse(parsed.user) : null, - group: parsed.group ? JSON.parse(parsed.group) : null, - } as CookieData; + setCurrentView = (view: View) => { + this.currentView = view; } - get loggedIn(): boolean { - if (this.cookieData === null) { - return false; - }; - return !!this.cookieData.user && !!this.cookieData.group; + setLoggedIn = (value: boolean) => { + this.loggedIn = value; + } + + setUser = (user: User | null) => { + this.user = user; + } + + setGroup = (group: Group | null) => { + this.group = group; + } + + logout = () => { + void cookieStore.delete('user'); + void cookieStore.delete('group'); + this.user = null; + this.group = null; + this.loggedIn = false; + this.currentView = 'signIn'; + } + + processCookie = () => { + const parsed = cookie.parse(document.cookie); + if (!parsed.user || !parsed.group) { + this.loggedIn = false; + return; + } + this.user = JSON.parse(parsed.user) as User[][0]; + this.group = JSON.parse(parsed.group) as Group[][0]; + + this.loggedIn = !!(this.user && this.group); } constructor() { - console.log('Initializing Store'); - this.processCookie(); makeAutoObservable(this); + this.processCookie(); + if (this.loggedIn) { + this.setCurrentView('group'); + } } } diff --git a/src/client/components/ForgotPassword.tsx b/src/client/components/ForgotPassword.tsx index 6558f31..b191d36 100644 --- a/src/client/components/ForgotPassword.tsx +++ b/src/client/components/ForgotPassword.tsx @@ -29,14 +29,12 @@ export default function ForgotPassword({ open, handleClose }: ForgotPasswordProp }} > Gruppencode vergessen - Gib deine E-Mail Adresse ein und wir senden dir eine E-Mail mit deinem Gruppencode zu. - - - diff --git a/src/client/components/Group.tsx b/src/client/components/Group.tsx index 7f9c9a8..727bec7 100644 --- a/src/client/components/Group.tsx +++ b/src/client/components/Group.tsx @@ -1,30 +1,26 @@ -import * as React from 'react'; import Box from '@mui/material/Box'; -import CssBaseline from '@mui/material/CssBaseline'; import Typography from '@mui/material/Typography'; import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; import Card from './Card'; import { useStore } from '../Store'; +import { CardHeader, IconButton } from '@mui/material'; +import LogoutIcon from '@mui/icons-material/Logout'; const Group = () => { - const [loading, setLoading] = React.useState(false); - const theme = useTheme(); const store = useStore(); - const { group } = store.cookieData!; + const { group, logout } = store; - console.log('Group component rendered with group:', group.name); + if (!group) { + return (Keine Gruppe gefunden.); + } return ( -
- +
{ }} > - - {group.name} - + + + + } + title={group.name} + /> { + const [emailInput, setEmailInput] = React.useState(''); + const [emailError, setEmailError] = React.useState(''); + const [groupInput, setGroupINput] = React.useState(''); + const [groupError, setGroupError] = React.useState(''); + const [open, setOpen] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + const {setLoggedIn, setCurrentView, setGroup, setUser} = useStore(); + const theme = useTheme(); + + React.useEffect(() => { + const params = new URLSearchParams(window.location.search); + const groupParam = params.get('gruppe'); + if (groupParam) { + setGroupINput(groupParam); + } + }, []); + + const handleClose = () => { + setOpen(false); + }; + + const handleCreateGroup = async (event: React.MouseEvent) => { + event.preventDefault(); + + if (!isMailValid() || !isGroupValid()) return; + + setLoading(true); + + try { + // handle user + let user = await fetchUser(emailInput); + + if (!user) { + user = await createUser(emailInput); + if (!user) throw new Error('Error creating user'); + } + + // handle group + const groupData = await createGroup({ name: groupInput, mail: emailInput }); + if (!groupData) throw new Error('Error creating group'); + + await cookieStore.set('user', JSON.stringify(user)); + await cookieStore.set('group', JSON.stringify(groupData)); + setUser(user); + setGroup(groupData); + + setLoggedIn(true); + setCurrentView('group'); + } + catch (error) { + console.error('Error during group creation:', error); + } finally { + setLoading(false); + } + }; + + const isMailValid = (): boolean => { + let isValid = true; + if (emailInput === '') { return true; } + if (!/\S+@\S+\.\S+/.test(emailInput)) { + setEmailError('Bitte gib eine gültige E-Mail Adresse ein.'); + isValid = false; + } else { + setEmailError(''); + } + return isValid; + }; + + const isGroupValid = (): boolean => { + let isValid = true; + if (groupInput === '') { return true; } + if (!/^[A-Z0-9]{6,}$/.test(groupInput)) { + setGroupError('Bitte gib einen gültigen Gruppenname ein. [A-Z, 0-9, mindestens 6 Zeichen]'); + isValid = false; + } else { + setGroupError(''); + } + return isValid; + }; + + const handleClickSignIn = () => { + setCurrentView('signIn'); + }; + + return ( +
+ + + + {'Neue Gruppe erstellen'} + + + + E-Mail des Gruppen-Administrator + setEmailInput(event.target.value.trim())} + value={emailInput} + color={emailError ? 'error' : 'primary'} /> + + + Gruppenname + void isGroupValid()} + onChange={event => setGroupINput(event.target.value.toUpperCase())} + value={groupInput} + color={groupError ? 'error' : 'primary'} + /> + + + + + oder + + + + + +
+ ); +}); + +export default NewGroup; \ No newline at end of file diff --git a/src/client/components/SignIn.tsx b/src/client/components/authentication/SignIn.tsx similarity index 83% rename from src/client/components/SignIn.tsx rename to src/client/components/authentication/SignIn.tsx index ba85af0..cfd828f 100644 --- a/src/client/components/SignIn.tsx +++ b/src/client/components/authentication/SignIn.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; -import CssBaseline from '@mui/material/CssBaseline'; import Divider from '@mui/material/Divider'; import FormLabel from '@mui/material/FormLabel'; import FormControl from '@mui/material/FormControl'; @@ -11,26 +10,28 @@ import Typography from '@mui/material/Typography'; import Stack from '@mui/material/Stack'; import { useTheme } from '@mui/material/styles'; import GroupAddIcon from '@mui/icons-material/GroupAdd'; -import ForgotPassword from './ForgotPassword'; -import Card from './Card'; -import { createUser, fetchGroupByCode, fetchUser } from '../serverApi'; +import ForgotPassword from '../ForgotPassword'; +import Card from '../Card'; +import { createUser, fetchGroupByCode, fetchUser } from '../../serverApi'; import { observer } from 'mobx-react-lite'; +import { useStore } from '../../Store'; const SignIn = observer(() => { - const [email, setEmail] = React.useState(''); + const [emailInput, setEmailInput] = React.useState(''); const [emailError, setEmailError] = React.useState(''); - const [group, setGroup] = React.useState(''); + const [groupInput, setGroupInput] = React.useState(''); const [groupError, setGroupError] = React.useState(''); const [open, setOpen] = React.useState(false); const [loading, setLoading] = React.useState(false); + const {setLoggedIn, setCurrentView, setGroup, setUser} = useStore(); const theme = useTheme(); React.useEffect(() => { const params = new URLSearchParams(window.location.search); const groupParam = params.get('gruppe'); if (groupParam) { - setGroup(groupParam); + setGroupInput(groupParam); } }, []); @@ -49,23 +50,27 @@ const SignIn = observer(() => { setLoading(true); try { - const groupData = await fetchGroupByCode(group); + const groupData = await fetchGroupByCode(groupInput); if (!groupData) { setGroupError('Gruppencode existiert nicht.'); return; } - let user = await fetchUser(email); + let user = await fetchUser(emailInput); if (!user) { - user = await createUser(email); + user = await createUser(emailInput); if (!user) throw new Error('Error creating user'); } await cookieStore.set('user', JSON.stringify(user)); await cookieStore.set('group', JSON.stringify(groupData)); + setGroup(groupData); + setUser(user); + setLoggedIn(true); + setCurrentView('group'); } catch (error) { console.error('Error during sign-in process:', error); @@ -75,7 +80,7 @@ const SignIn = observer(() => { const isMailValid = (): boolean => { let isValid = true; - if (!/\S+@\S+\.\S+/.test(email)) { + if (!/\S+@\S+\.\S+/.test(emailInput)) { setEmailError('Bitte gib eine gültige E-Mail Adresse ein.'); isValid = false; } else { @@ -86,7 +91,7 @@ const SignIn = observer(() => { const isGroupValid = (): boolean => { let isValid = true; - if (!/^[A-Z0-9]{6}$/.test(group)) { + if (!/^[A-Z0-9]{6}$/.test(groupInput)) { setGroupError('Bitte gib einen gültigen Gruppencode ein.'); isValid = false; } else { @@ -95,15 +100,15 @@ const SignIn = observer(() => { return isValid; }; + const handleClickNewGroup = () => { + setCurrentView('newGroup'); + } + return ( -
- +
{ name="email" placeholder="ren@tier.de" autoComplete="email" - autoFocus required fullWidth variant="outlined" onBlur={isMailValid} - onChange={event => setEmail(event.target.value)} - value={email} + onChange={event => setEmailInput(event.target.value)} + value={emailInput} color={emailError ? 'error' : 'primary'} /> @@ -160,8 +164,8 @@ const SignIn = observer(() => { fullWidth variant="outlined" onBlur={isGroupValid} - onChange={event => setGroup(event.target.value)} - value={group} + onChange={event => setGroupInput(event.target.value.toUpperCase())} + value={groupInput} color={groupError ? 'error' : 'primary'} /> @@ -188,7 +192,7 @@ const SignIn = observer(() => {