add group creation
This commit is contained in:
6
bun.lock
6
bun.lock
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
|
"configVersion": 0,
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"": {
|
"": {
|
||||||
"name": "bun-react-template",
|
"name": "bun-react-template",
|
||||||
@@ -10,7 +11,8 @@
|
|||||||
"@mui/icons-material": "7.3.5",
|
"@mui/icons-material": "7.3.5",
|
||||||
"@mui/material": "7.3.5",
|
"@mui/material": "7.3.5",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"mobx-react-lite": "^4.1.1",
|
"mobx": "^6.15.0",
|
||||||
|
"mobx-react": "^9.2.1",
|
||||||
"react": "19",
|
"react": "19",
|
||||||
"react-dom": "19",
|
"react-dom": "19",
|
||||||
"react-router-dom": "^7.9.5",
|
"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": ["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=="],
|
"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=="],
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|||||||
BIN
data.sqlite
BIN
data.sqlite
Binary file not shown.
@@ -16,7 +16,8 @@
|
|||||||
"@mui/icons-material": "7.3.5",
|
"@mui/icons-material": "7.3.5",
|
||||||
"@mui/material": "7.3.5",
|
"@mui/material": "7.3.5",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
"mobx-react-lite": "^4.1.1",
|
"mobx": "^6.15.0",
|
||||||
|
"mobx-react": "^9.2.1",
|
||||||
"react": "19",
|
"react": "19",
|
||||||
"react-dom": "19",
|
"react-dom": "19",
|
||||||
"react-router-dom": "^7.9.5"
|
"react-router-dom": "^7.9.5"
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
import SignIn from './components/SignIn';
|
import SignIn from './components/authentication/SignIn';
|
||||||
import { createTheme, ThemeProvider } from '@mui/material';
|
import { createTheme, CssBaseline, ThemeProvider } from '@mui/material';
|
||||||
import { useStore } from './Store';
|
import { useStore, type View } from './Store';
|
||||||
import Group from './components/Group';
|
import Group from './components/Group';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { type ReactElement } from 'react';
|
||||||
|
import NewGroup from './components/authentication/NewGroup';
|
||||||
|
|
||||||
|
export const views: Record<View, ReactElement> = {
|
||||||
|
signIn: <SignIn />,
|
||||||
|
group: <Group />,
|
||||||
|
newGroup: <NewGroup />,
|
||||||
|
};
|
||||||
|
|
||||||
const App = observer(() => {
|
const App = observer(() => {
|
||||||
const theme = createTheme({
|
const theme = createTheme({
|
||||||
@@ -12,15 +20,18 @@ const App = observer(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { loggedIn } = store;
|
const { currentView } = store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<div className="app">
|
<CssBaseline enableColorScheme />
|
||||||
{loggedIn ?
|
<div className="app"
|
||||||
<Group /> :
|
style={{
|
||||||
<SignIn />
|
backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))',
|
||||||
}
|
backgroundRepeat: 'no-repeat',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{views[currentView]}
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,36 +1,64 @@
|
|||||||
import { createContext, useContext } from 'react';
|
import { createContext, useContext } from 'react';
|
||||||
import { makeAutoObservable } from 'mobx';
|
|
||||||
import cookie from 'cookie';
|
import cookie from 'cookie';
|
||||||
import type { Group, User } from '@/interfaces';
|
import type { Group, User } from '@/interfaces';
|
||||||
|
import { makeAutoObservable } from 'mobx';
|
||||||
|
|
||||||
export type CookieData = {
|
export type CookieData = {
|
||||||
user: User;
|
user: User;
|
||||||
group: Group;
|
group: Group;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type View = 'signIn' | 'group' | 'newGroup';
|
||||||
|
|
||||||
export class Store {
|
export class Store {
|
||||||
cookieData: CookieData | null = null;
|
loggedIn = false;
|
||||||
|
currentView: View = 'signIn';
|
||||||
|
user: User | null = null;
|
||||||
|
group: Group | null = null;
|
||||||
|
|
||||||
processCookie() {
|
setCurrentView = (view: View) => {
|
||||||
const parsed = cookie.parse(document.cookie);
|
this.currentView = view;
|
||||||
|
|
||||||
this.cookieData = {
|
|
||||||
user: parsed.user ? JSON.parse(parsed.user) : null,
|
|
||||||
group: parsed.group ? JSON.parse(parsed.group) : null,
|
|
||||||
} as CookieData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get loggedIn(): boolean {
|
setLoggedIn = (value: boolean) => {
|
||||||
if (this.cookieData === null) {
|
this.loggedIn = value;
|
||||||
return false;
|
}
|
||||||
};
|
|
||||||
return !!this.cookieData.user && !!this.cookieData.group;
|
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() {
|
constructor() {
|
||||||
console.log('Initializing Store');
|
|
||||||
this.processCookie();
|
|
||||||
makeAutoObservable(this);
|
makeAutoObservable(this);
|
||||||
|
this.processCookie();
|
||||||
|
if (this.loggedIn) {
|
||||||
|
this.setCurrentView('group');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,14 +29,12 @@ export default function ForgotPassword({ open, handleClose }: ForgotPasswordProp
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle>Gruppencode vergessen</DialogTitle>
|
<DialogTitle>Gruppencode vergessen</DialogTitle>
|
||||||
|
|
||||||
<DialogContent
|
<DialogContent
|
||||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, width: '100%' }}
|
sx={{ display: 'flex', flexDirection: 'column', gap: 2, width: '100%' }}
|
||||||
>
|
>
|
||||||
<DialogContentText>
|
<DialogContentText>
|
||||||
Gib deine E-Mail Adresse ein und wir senden dir eine E-Mail mit deinem Gruppencode zu.
|
Gib deine E-Mail Adresse ein und wir senden dir eine E-Mail mit deinem Gruppencode zu.
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
|
|
||||||
<OutlinedInput
|
<OutlinedInput
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
@@ -49,10 +47,8 @@ export default function ForgotPassword({ open, handleClose }: ForgotPasswordProp
|
|||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
<DialogActions sx={{ pb: 3, px: 3 }}>
|
<DialogActions sx={{ pb: 3, px: 3 }}>
|
||||||
<Button onClick={handleClose}>Cancel</Button>
|
<Button onClick={handleClose}>Cancel</Button>
|
||||||
|
|
||||||
<Button variant="contained" type="submit">
|
<Button variant="contained" type="submit">
|
||||||
Continue
|
Continue
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,30 +1,26 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import Card from './Card';
|
import Card from './Card';
|
||||||
import { useStore } from '../Store';
|
import { useStore } from '../Store';
|
||||||
|
import { CardHeader, IconButton } from '@mui/material';
|
||||||
|
import LogoutIcon from '@mui/icons-material/Logout';
|
||||||
|
|
||||||
const Group = () => {
|
const Group = () => {
|
||||||
const [loading, setLoading] = React.useState(false);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
const { group } = store.cookieData!;
|
const { group, logout } = store;
|
||||||
|
|
||||||
console.log('Group component rendered with group:', group.name);
|
if (!group) {
|
||||||
|
return (<Typography>Keine Gruppe gefunden.</Typography>);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div>
|
||||||
backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}>
|
|
||||||
<CssBaseline enableColorScheme />
|
|
||||||
<Stack direction="column" justifyContent="space-between"
|
<Stack direction="column" justifyContent="space-between"
|
||||||
sx={{
|
sx={{
|
||||||
height: 'calc((1 - var(--template-frame-height, 0)) * 100dvh)',
|
height: '100dvh',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up('sm')]: {
|
||||||
@@ -33,13 +29,14 @@ const Group = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Card variant="outlined">
|
<Card variant="outlined">
|
||||||
<Typography
|
<CardHeader
|
||||||
component="h1"
|
action={
|
||||||
variant="h4"
|
<IconButton aria-label="logout" onClick={logout}>
|
||||||
sx={{ width: '100%', fontSize: 'clamp(2rem, 10vw, 2.15rem)' }}
|
<LogoutIcon />
|
||||||
>
|
</IconButton>
|
||||||
{group.name}
|
}
|
||||||
</Typography>
|
title={group.name}
|
||||||
|
/>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|||||||
196
src/client/components/authentication/NewGroup.tsx
Normal file
196
src/client/components/authentication/NewGroup.tsx
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Divider from '@mui/material/Divider';
|
||||||
|
import FormLabel from '@mui/material/FormLabel';
|
||||||
|
import FormControl from '@mui/material/FormControl';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Stack from '@mui/material/Stack';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import ForgotPassword from '../ForgotPassword';
|
||||||
|
import Card from '../Card';
|
||||||
|
import { createGroup, createUser, fetchUser } from '../../serverApi';
|
||||||
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { useStore } from '../../Store';
|
||||||
|
import LoginIcon from '@mui/icons-material/Login';
|
||||||
|
|
||||||
|
const NewGroup = observer(() => {
|
||||||
|
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<HTMLButtonElement, 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 (
|
||||||
|
<div>
|
||||||
|
<Stack direction="column" justifyContent="space-between"
|
||||||
|
sx={{
|
||||||
|
height: '100dvh',
|
||||||
|
minHeight: '100%',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
[theme.breakpoints.up('sm')]: {
|
||||||
|
padding: theme.spacing(4),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Card variant="outlined">
|
||||||
|
<Typography
|
||||||
|
component="h1"
|
||||||
|
variant="h4"
|
||||||
|
sx={{ width: '100%', fontSize: 'clamp(2rem, 10vw, 2.15rem)' }}
|
||||||
|
>
|
||||||
|
{'Neue Gruppe erstellen'}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
width: '100%',
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel htmlFor="email">E-Mail des Gruppen-Administrator</FormLabel>
|
||||||
|
<TextField
|
||||||
|
error={emailError !== ''}
|
||||||
|
helperText={emailError}
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
placeholder="ren@tier.de"
|
||||||
|
autoComplete="email"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onBlur={isMailValid}
|
||||||
|
onChange={event => setEmailInput(event.target.value.trim())}
|
||||||
|
value={emailInput}
|
||||||
|
color={emailError ? 'error' : 'primary'} />
|
||||||
|
</FormControl>
|
||||||
|
<FormControl>
|
||||||
|
<FormLabel>Gruppenname</FormLabel>
|
||||||
|
<TextField
|
||||||
|
error={groupError !== ''}
|
||||||
|
helperText={groupError}
|
||||||
|
name="groupcode"
|
||||||
|
placeholder="GRUPPE123"
|
||||||
|
type="text"
|
||||||
|
id="groupcode"
|
||||||
|
autoComplete="current-groupcode"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onBlur={() => void isGroupValid()}
|
||||||
|
onChange={event => setGroupINput(event.target.value.toUpperCase())}
|
||||||
|
value={groupInput}
|
||||||
|
color={groupError ? 'error' : 'primary'}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<ForgotPassword open={open} handleClose={handleClose} />
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="contained"
|
||||||
|
onClick={event => {void handleCreateGroup(event)}}
|
||||||
|
loading={loading}
|
||||||
|
>
|
||||||
|
{'Gruppe erstellen'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Divider>oder</Divider>
|
||||||
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleClickSignIn}
|
||||||
|
startIcon={<LoginIcon />}
|
||||||
|
>
|
||||||
|
{'Gruppe beitreten'}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default NewGroup;
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import FormLabel from '@mui/material/FormLabel';
|
import FormLabel from '@mui/material/FormLabel';
|
||||||
import FormControl from '@mui/material/FormControl';
|
import FormControl from '@mui/material/FormControl';
|
||||||
@@ -11,26 +10,28 @@ import Typography from '@mui/material/Typography';
|
|||||||
import Stack from '@mui/material/Stack';
|
import Stack from '@mui/material/Stack';
|
||||||
import { useTheme } from '@mui/material/styles';
|
import { useTheme } from '@mui/material/styles';
|
||||||
import GroupAddIcon from '@mui/icons-material/GroupAdd';
|
import GroupAddIcon from '@mui/icons-material/GroupAdd';
|
||||||
import ForgotPassword from './ForgotPassword';
|
import ForgotPassword from '../ForgotPassword';
|
||||||
import Card from './Card';
|
import Card from '../Card';
|
||||||
import { createUser, fetchGroupByCode, fetchUser } from '../serverApi';
|
import { createUser, fetchGroupByCode, fetchUser } from '../../serverApi';
|
||||||
import { observer } from 'mobx-react-lite';
|
import { observer } from 'mobx-react-lite';
|
||||||
|
import { useStore } from '../../Store';
|
||||||
|
|
||||||
const SignIn = observer(() => {
|
const SignIn = observer(() => {
|
||||||
const [email, setEmail] = React.useState('');
|
const [emailInput, setEmailInput] = React.useState('');
|
||||||
const [emailError, setEmailError] = React.useState('');
|
const [emailError, setEmailError] = React.useState('');
|
||||||
const [group, setGroup] = React.useState('');
|
const [groupInput, setGroupInput] = React.useState('');
|
||||||
const [groupError, setGroupError] = React.useState('');
|
const [groupError, setGroupError] = React.useState('');
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [loading, setLoading] = React.useState(false);
|
const [loading, setLoading] = React.useState(false);
|
||||||
|
|
||||||
|
const {setLoggedIn, setCurrentView, setGroup, setUser} = useStore();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const groupParam = params.get('gruppe');
|
const groupParam = params.get('gruppe');
|
||||||
if (groupParam) {
|
if (groupParam) {
|
||||||
setGroup(groupParam);
|
setGroupInput(groupParam);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -49,23 +50,27 @@ const SignIn = observer(() => {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const groupData = await fetchGroupByCode(group);
|
const groupData = await fetchGroupByCode(groupInput);
|
||||||
|
|
||||||
if (!groupData) {
|
if (!groupData) {
|
||||||
setGroupError('Gruppencode existiert nicht.');
|
setGroupError('Gruppencode existiert nicht.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let user = await fetchUser(email);
|
let user = await fetchUser(emailInput);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
user = await createUser(email);
|
user = await createUser(emailInput);
|
||||||
if (!user) throw new Error('Error creating user');
|
if (!user) throw new Error('Error creating user');
|
||||||
}
|
}
|
||||||
|
|
||||||
await cookieStore.set('user', JSON.stringify(user));
|
await cookieStore.set('user', JSON.stringify(user));
|
||||||
await cookieStore.set('group', JSON.stringify(groupData));
|
await cookieStore.set('group', JSON.stringify(groupData));
|
||||||
|
setGroup(groupData);
|
||||||
|
setUser(user);
|
||||||
|
|
||||||
|
setLoggedIn(true);
|
||||||
|
setCurrentView('group');
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
console.error('Error during sign-in process:', error);
|
console.error('Error during sign-in process:', error);
|
||||||
@@ -75,7 +80,7 @@ const SignIn = observer(() => {
|
|||||||
|
|
||||||
const isMailValid = (): boolean => {
|
const isMailValid = (): boolean => {
|
||||||
let isValid = true;
|
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.');
|
setEmailError('Bitte gib eine gültige E-Mail Adresse ein.');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -86,7 +91,7 @@ const SignIn = observer(() => {
|
|||||||
|
|
||||||
const isGroupValid = (): boolean => {
|
const isGroupValid = (): boolean => {
|
||||||
let isValid = true;
|
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.');
|
setGroupError('Bitte gib einen gültigen Gruppencode ein.');
|
||||||
isValid = false;
|
isValid = false;
|
||||||
} else {
|
} else {
|
||||||
@@ -95,15 +100,15 @@ const SignIn = observer(() => {
|
|||||||
return isValid;
|
return isValid;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleClickNewGroup = () => {
|
||||||
|
setCurrentView('newGroup');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div>
|
||||||
backgroundImage: 'radial-gradient(at 50% 50%, hsla(210, 100%, 16%, 0.5), hsl(220, 30%, 5%))',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
}}>
|
|
||||||
<CssBaseline enableColorScheme />
|
|
||||||
<Stack direction="column" justifyContent="space-between"
|
<Stack direction="column" justifyContent="space-between"
|
||||||
sx={{
|
sx={{
|
||||||
height: 'calc((1 - var(--template-frame-height, 0)) * 100dvh)',
|
height: '100dvh',
|
||||||
minHeight: '100%',
|
minHeight: '100%',
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up('sm')]: {
|
||||||
@@ -137,13 +142,12 @@ const SignIn = observer(() => {
|
|||||||
name="email"
|
name="email"
|
||||||
placeholder="ren@tier.de"
|
placeholder="ren@tier.de"
|
||||||
autoComplete="email"
|
autoComplete="email"
|
||||||
autoFocus
|
|
||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onBlur={isMailValid}
|
onBlur={isMailValid}
|
||||||
onChange={event => setEmail(event.target.value)}
|
onChange={event => setEmailInput(event.target.value)}
|
||||||
value={email}
|
value={emailInput}
|
||||||
color={emailError ? 'error' : 'primary'} />
|
color={emailError ? 'error' : 'primary'} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
@@ -160,8 +164,8 @@ const SignIn = observer(() => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onBlur={isGroupValid}
|
onBlur={isGroupValid}
|
||||||
onChange={event => setGroup(event.target.value)}
|
onChange={event => setGroupInput(event.target.value.toUpperCase())}
|
||||||
value={group}
|
value={groupInput}
|
||||||
color={groupError ? 'error' : 'primary'} />
|
color={groupError ? 'error' : 'primary'} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<ForgotPassword open={open} handleClose={handleClose} />
|
<ForgotPassword open={open} handleClose={handleClose} />
|
||||||
@@ -188,7 +192,7 @@ const SignIn = observer(() => {
|
|||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
onClick={() => alert('Sign in with Google')}
|
onClick={handleClickNewGroup}
|
||||||
startIcon={<GroupAddIcon />}
|
startIcon={<GroupAddIcon />}
|
||||||
>
|
>
|
||||||
{'Neue Gruppe erstellen'}
|
{'Neue Gruppe erstellen'}
|
||||||
@@ -34,7 +34,7 @@ export async function fetchGroupByCode(code: string): Promise<Group | null> {
|
|||||||
try {
|
try {
|
||||||
const res = await fetch(`/api/group/${code}`);
|
const res = await fetch(`/api/group/${code}`);
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const data: Group = await res.json();
|
const data: Group | null = await res.json();
|
||||||
return data;
|
return data;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch group:', err);
|
console.error('Failed to fetch group:', err);
|
||||||
@@ -42,14 +42,14 @@ export async function fetchGroupByCode(code: string): Promise<Group | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createGroup(group: Group): Promise<Group | null> {
|
export async function createGroup(group: Partial<Group>): Promise<Group | null> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/group', {
|
const res = await fetch('/api/group', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ group }),
|
body: JSON.stringify(group),
|
||||||
});
|
});
|
||||||
if (!res.ok) return null;
|
if (!res.ok) return null;
|
||||||
const data: Group = await res.json();
|
const data: Group = await res.json();
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ const server = serve({
|
|||||||
console.log('Received request for user:', mail);
|
console.log('Received request for user:', mail);
|
||||||
const user = await db.getUserByMail(mail);
|
const user = await db.getUserByMail(mail);
|
||||||
console.log('Fetching user with mail:', mail, 'Result:', user);
|
console.log('Fetching user with mail:', mail, 'Result:', user);
|
||||||
if (!user) return new Response(JSON.stringify({ error: 'User not found' }), { status: 404 });
|
|
||||||
return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' } });
|
return new Response(JSON.stringify(user), { headers: { 'Content-Type': 'application/json' } });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -37,7 +36,6 @@ const server = serve({
|
|||||||
console.log('Received request for group:', code);
|
console.log('Received request for group:', code);
|
||||||
const group = await db.getGroupByCode(code);
|
const group = await db.getGroupByCode(code);
|
||||||
console.log('Fetching group with ID:', code, 'Result:', group);
|
console.log('Fetching group with ID:', code, 'Result:', group);
|
||||||
if (!group) return new Response(JSON.stringify({ error: 'Group not found' }), { status: 404 });
|
|
||||||
return new Response(JSON.stringify(group), { headers: { 'Content-Type': 'application/json' } });
|
return new Response(JSON.stringify(group), { headers: { 'Content-Type': 'application/json' } });
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -46,7 +44,7 @@ const server = serve({
|
|||||||
try {
|
try {
|
||||||
const { name, mail } = await req.json() as { name: string; mail: string };
|
const { name, mail } = await req.json() as { name: string; mail: string };
|
||||||
console.log('Received request to create group with name:', name, 'and mail:', mail);
|
console.log('Received request to create group with name:', name, 'and mail:', mail);
|
||||||
const response = db.createGroup({ name, mail });
|
const response = await db.createGroup({ name, mail });
|
||||||
return new Response(JSON.stringify(response), { headers: { 'Content-Type': 'application/json' } });
|
return new Response(JSON.stringify(response), { headers: { 'Content-Type': 'application/json' } });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creating group:', err);
|
console.error('Error creating group:', err);
|
||||||
|
|||||||
@@ -80,25 +80,25 @@ class DB {
|
|||||||
* @param mail: string
|
* @param mail: string
|
||||||
* @returns created user
|
* @returns created user
|
||||||
*/
|
*/
|
||||||
public async createUser(mail: string): Promise<User> {
|
public async createUser(mail: string): Promise<User | null> {
|
||||||
const user: User = await this.instance`
|
const user: User[] = await this.instance`
|
||||||
INSERT INTO users (mail) VALUES (${mail})
|
INSERT INTO users (mail) VALUES (${mail})
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return user;
|
return user[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user by mail
|
* Get user by mail
|
||||||
* @param mail: string
|
* @param mail: string
|
||||||
* @returns user object or undefined
|
* @returns user object or null
|
||||||
*/
|
*/
|
||||||
public async getUserByMail(mail: string): Promise<User | undefined> {
|
public async getUserByMail(mail: string): Promise<User | null> {
|
||||||
const user: User = await this.instance`
|
const user: User[] = await this.instance`
|
||||||
SELECT * FROM users WHERE mail = ${mail}
|
SELECT * FROM users WHERE mail = ${mail}
|
||||||
`;
|
`;
|
||||||
return user;
|
return user[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GROUPS */
|
/* GROUPS */
|
||||||
@@ -109,25 +109,26 @@ class DB {
|
|||||||
* @param mail: string
|
* @param mail: string
|
||||||
* @returns object with id of the created group
|
* @returns object with id of the created group
|
||||||
*/
|
*/
|
||||||
public async createGroup( {name, mail}: {name: string, mail: string}): Promise<Group> {
|
public async createGroup( {name, mail}: {name: string, mail: string}): Promise<Group | null> {
|
||||||
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
|
const code = Math.random().toString(36).substring(2, 8).toUpperCase();
|
||||||
const group: Group = await this.instance`
|
const group: Group[] = await this.instance`
|
||||||
INSERT INTO groups (code, name, mail) VALUES (${code}, ${name}, ${mail})
|
INSERT INTO groups (code, name, mail) VALUES (${code}, ${name}, ${mail})
|
||||||
RETURNING *
|
RETURNING *
|
||||||
`;
|
`;
|
||||||
return group;
|
console.log('Created group:', group);
|
||||||
|
return group[0] ?? null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get group by ID
|
* Get group by ID
|
||||||
* @param id: string
|
* @param id: string
|
||||||
* @returns group object or undefined
|
* @returns group object or null
|
||||||
*/
|
*/
|
||||||
public async getGroupByCode(code: string): Promise<Group | undefined> {
|
public async getGroupByCode(code: string): Promise<Group | null> {
|
||||||
const group: Group | undefined = await this.instance`
|
const group: Group[] = await this.instance`
|
||||||
SELECT * FROM groups WHERE code = ${code}
|
SELECT * FROM groups WHERE code = ${code}
|
||||||
`;
|
`;
|
||||||
return group;
|
return group[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GROUP MEMBER */
|
/* GROUP MEMBER */
|
||||||
|
|||||||
Reference in New Issue
Block a user