From 624e62822c12aec0fe86b9f8789816578ea35529 Mon Sep 17 00:00:00 2001 From: Sebastian Brenner Date: Wed, 7 Jan 2026 10:25:33 +0100 Subject: [PATCH] add group creation --- bun.lock | 6 +- data.sqlite | Bin 49152 -> 49152 bytes package.json | 3 +- src/client/App.tsx | 29 ++- src/client/Store.ts | 60 ++++-- src/client/components/ForgotPassword.tsx | 4 - src/client/components/Group.tsx | 35 ++-- .../components/authentication/NewGroup.tsx | 196 ++++++++++++++++++ .../{ => authentication}/SignIn.tsx | 52 ++--- src/client/serverApi.ts | 6 +- src/index.ts | 4 +- src/server/db.ts | 29 +-- 12 files changed, 330 insertions(+), 94 deletions(-) create mode 100644 src/client/components/authentication/NewGroup.tsx rename src/client/components/{ => authentication}/SignIn.tsx (83%) 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 976d2c53fd0e8f3e81086e2a22b3164cbfd85a0c..944683265977e2d9d8b768ef5ca03cda8edffa62 100644 GIT binary patch delta 465 zcmZo@U~Xt&o*>O=I8nx#(QsqJ5`G>g{x$~w!~AXhp8U+41qD9y)!VUfFi6TPGsqj8 zScZ89IJpOf1_Zbo8W|U*<~fvPrWWa?q^2j9WTX~l=A|1M7@6rB80s3BD;QW)5O$_ zm;V(5Gyi!8{;&M!`ELWYALMuBVgjm6FUl`1DCXm3W>)0PP0dY8Eh^5)EMQ<@;O2x# zaWXQCGnN(u#W{I_!YurG3_urbOAHc`fzQEX$v5`GRw{=*FXhxt1<3o6v|TUju2Fi6TuGRPa7dAqwA z8>FO`r5ELw79=_(>7}HmCzfQS7G>t88yOgx=^7a78ki^;8d#YcTbY_oJ}mDDH0&D# z|2O_On*{?N@GEgLOL9Vt0kSv@jf_o9&3G9Y7?}BQGw^@qf5m?rsO~JkAulttB4=)D xZc=JdaYkkVClgQ~*w$iBMrLuw(qf=EJ4hoF|278x2mITBZe7K{d7Hmi0RSelIKTh^ 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(() => {