Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • kszk/devteam/org/bodysch/bodysch-frontend
1 result
Show changes
Commits on Source (13)
Showing
with 19752 additions and 2273 deletions
Source diff could not be displayed: it is too large. Options to address this: view the blob.
import React from 'react'; import React from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
import { BrowserRouter as Router } from 'react-router-dom'; import { BrowserRouter as Router } from 'react-router-dom';
import RememberMe from './components/RememberMe';
import { ClientContextProvider } from './context'; import { ClientContextProvider } from './context';
import ThemeProvider from './context/ThemeProvider'; import ThemeProvider from './context/ThemeProvider';
import { UserStateProvider } from './context/UserContext'; import { UserStateProvider } from './context/UserContext';
import Routes from './Routes'; import Routes from './Routes';
import darkTheme from './styles/darkTheme'; import darkTheme from './styles/darkTheme';
const queryClient = new QueryClient();
function App(): React.ReactElement { function App(): React.ReactElement {
return ( return (
<ClientContextProvider> <QueryClientProvider client={queryClient}>
<UserStateProvider> <ClientContextProvider>
<ThemeProvider theme={darkTheme}> <UserStateProvider>
<Router> <ThemeProvider theme={darkTheme}>
<Routes /> <Router>
</Router> <RememberMe />
</ThemeProvider> <Routes />
</UserStateProvider> </Router>
</ClientContextProvider> </ThemeProvider>
</UserStateProvider>
</ClientContextProvider>
</QueryClientProvider>
); );
} }
......
import React from 'react'; import React from 'react';
import { Redirect, Route, Switch } from 'react-router'; import { Redirect, Route, Switch } from 'react-router';
import NewsPage from './pages/NewsPage'; import NewsPage from './pages/NewsPage';
import RegisterPage from './pages/RegisterPage';
import RulesPage from './pages/RulesPage'; import RulesPage from './pages/RulesPage';
const Routes: React.FC = () => ( const Routes: React.FC = () => (
...@@ -14,6 +15,9 @@ const Routes: React.FC = () => ( ...@@ -14,6 +15,9 @@ const Routes: React.FC = () => (
<Route path="/rules"> <Route path="/rules">
<RulesPage /> <RulesPage />
</Route> </Route>
<Route path="/register">
<RegisterPage />
</Route>
</Switch> </Switch>
); );
......
import Axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; import Axios, { AxiosError, AxiosRequestConfig } from 'axios';
import { EmptyReq, LoginReq, RegisterReq, UserResponse } from './types'; import type { ProfileWithStatus } from '../context';
import { IProfile } from '../types/Profile';
type LoginQuery = (data?: LoginReq) => Promise<AxiosResponse<UserResponse>>; type MeQuery = () => Promise<ProfileWithStatus>;
type RegisterQuery = (data?: RegisterReq) => Promise<AxiosResponse<UserResponse>>; type RegisterQuery = (data: ProfileData) => Promise<any>;
type MeQuery = (data?: EmptyReq, authToken?: string) => Promise<AxiosResponse<UserResponse>>;
export type ProfileData = {
studentCardNumber: string;
roomNumber: number;
};
export type Id = {
_id: string;
};
export type AuthClient = { export type AuthClient = {
login: LoginQuery;
me: MeQuery; me: MeQuery;
register: RegisterQuery; register: RegisterQuery;
}; };
...@@ -14,29 +22,28 @@ export type AuthClient = { ...@@ -14,29 +22,28 @@ export type AuthClient = {
export function auth(config: AxiosRequestConfig): AuthClient { export function auth(config: AxiosRequestConfig): AuthClient {
const axios = Axios.create({ const axios = Axios.create({
...config, ...config,
baseURL: `${config.baseURL}/api/auth`, baseURL: `${config.baseURL}/api/v1/`,
}); });
const login: LoginQuery = (data?: LoginReq): Promise<AxiosResponse<UserResponse>> => { const me: MeQuery = async (): Promise<ProfileWithStatus> => {
return axios.post<UserResponse>('/login', data); try {
const profileResp = await axios.get<IProfile>('/users/me');
const isLoggedIn =
profileResp.status === 404 || profileResp.status === 200 || profileResp.status === 403;
return { profile: profileResp.data, isLoggedIn };
} catch (e) {
const err = e as AxiosError;
const isLoggedIn = (err.response?.status === 404 || err.response?.status === 403) ?? false;
return { profile: undefined, isLoggedIn };
}
}; };
const register: RegisterQuery = (data?: RegisterReq): Promise<AxiosResponse<UserResponse>> => { const register: RegisterQuery = async (data: ProfileData): Promise<Id> => {
return axios.post<UserResponse>('/register', data); return (await axios.post('/users/me', data)).data;
};
const me: MeQuery = (
data?: undefined,
authToken?: string,
): Promise<AxiosResponse<UserResponse>> => {
return axios.get<UserResponse>('/me', {
headers: { Authorization: `Bearer ${authToken}` },
});
}; };
return { return {
login,
register,
me, me,
register,
}; };
} }
...@@ -89,10 +89,9 @@ const useStyles = makeStyles((theme) => ({ ...@@ -89,10 +89,9 @@ const useStyles = makeStyles((theme) => ({
export type HeaderProps = { export type HeaderProps = {
customToolbar?: React.ReactElement; customToolbar?: React.ReactElement;
menuItems?: AppBarMenuItem[]; menuItems?: AppBarMenuItem[];
handleLogout?: () => void;
}; };
const Header: React.FC<HeaderProps> = ({ customToolbar, handleLogout, menuItems = MENU_ITEMS }) => { const Header: React.FC<HeaderProps> = ({ customToolbar, menuItems = MENU_ITEMS }) => {
const classes = useStyles(); const classes = useStyles();
const history = useHistory(); const history = useHistory();
const { profile } = useUserContext(); const { profile } = useUserContext();
...@@ -143,7 +142,7 @@ const Header: React.FC<HeaderProps> = ({ customToolbar, handleLogout, menuItems ...@@ -143,7 +142,7 @@ const Header: React.FC<HeaderProps> = ({ customToolbar, handleLogout, menuItems
/> />
))} ))}
{profile.role === 'ADMIN' && ( {profile.profile && profile.profile.role === 'ADMIN' && (
<> <>
{ADMIN_MENU_ITEMS.map((item) => ( {ADMIN_MENU_ITEMS.map((item) => (
<Tab <Tab
...@@ -162,35 +161,35 @@ const Header: React.FC<HeaderProps> = ({ customToolbar, handleLogout, menuItems ...@@ -162,35 +161,35 @@ const Header: React.FC<HeaderProps> = ({ customToolbar, handleLogout, menuItems
</Tabs> </Tabs>
</Grid> </Grid>
{!isNil(profile) ? ( {!isNil(profile) && profile.profile ? (
<> <>
<IconButton size="medium"> <IconButton size="medium">
<Badge <Badge color="error">
badgeContent={profile.notices.filter((item) => !item.isSeen).length}
color="error"
>
<MailOutline style={{ color: 'white' }} /> <MailOutline style={{ color: 'white' }} />
</Badge> </Badge>
</IconButton> </IconButton>
{profile.warnings.length > 0 && ( <IconButton size="medium" disableRipple>
<IconButton size="medium" disableRipple> <Report color="error" />
<Report color="error" /> </IconButton>
</IconButton>
)}
<Typography variant="h6" align="center" className={classes.texts}> <Typography variant="h6" align="center" className={classes.texts}>
{profile.name} {profile.profile && profile.profile.name}
</Typography> </Typography>
<Box color="secondary.main"> <a href="/api/v1/logout">
<IconButton color="inherit" size="medium" onClick={handleLogout}> <Box color="secondary.main">
<ExitToApp /> <IconButton color="inherit" size="medium">
</IconButton> <ExitToApp />
</Box> </IconButton>
</Box>
</a>
</> </>
) : ( ) : (
<Button color="secondary" variant="contained"> <a href="/api/v1/login">
Belépés <Button color="secondary" variant="contained">
</Button> Belépés
</Button>
</a>
)} )}
</Toolbar> </Toolbar>
)} )}
......
import { Button } from '@material-ui/core';
import React, { useState } from 'react';
import { ProfileModal } from './ProfileModal';
const ProfileButton: React.FC = () => {
const [isProfileModalOpen, setIsProfileModalOpen] = useState<boolean>(true);
const handleProfileModalOpen = () => {
setIsProfileModalOpen(true);
};
return (
<div>
<Button variant="contained" color="primary" onClick={handleProfileModalOpen}>
Open Profile
</Button>
<ProfileModal isOpen={isProfileModalOpen} setIsOpen={setIsProfileModalOpen} />
</div>
);
};
export default ProfileButton;
// eslint-disable-next-line object-curly-newline
import { Avatar, Box, Button, Container, Typography } from '@material-ui/core';
import { Field, Form, Formik } from 'formik';
import { isNil } from 'lodash';
import React, { useEffect, useState } from 'react';
import { useQueryClient } from 'react-query';
import { Redirect } from 'react-router-dom';
import useRegister from '../hooks/useRegister';
import StyledTextField from './StyledTextField';
type FormValues = {
roomNumber: string | undefined;
studentCardNumber: string;
};
const ProfileForm: React.FC = () => {
const { mutate: register, data, isError } = useRegister();
const [redirect, setRedirect] = useState(false);
const client = useQueryClient();
useEffect(() => {
if (data && !isError) {
client.invalidateQueries('me');
setRedirect(true);
}
}, [data, isError]);
return (
<Container
style={{
padding: '15px',
width: '500px',
borderRadius: '8px',
}}
>
<Formik
initialValues={{ roomNumber: '', studentCardNumber: '' } as FormValues}
onSubmit={async ({ roomNumber, studentCardNumber }): Promise<void> => {
if (!isNil(roomNumber) && !isNil(studentCardNumber)) {
await register({ roomNumber: Number(roomNumber), studentCardNumber });
}
}}
>
<Form>
<Box
fontWeight="fontWeightBold"
display="flex"
justifyContent="space-between"
alignItems="center"
pl={2}
>
<Typography variant="h5" component="h5" color="textPrimary">
Profilom
</Typography>
</Box>
<Box display="flex" justifyContent="center" my={3}>
<Avatar
alt="Profile image"
variant="rounded"
style={{ width: '200px', height: '200px' }}
/>
</Box>
<Box px={1} mt={2} mb={1}>
<Box my={1}>
<Field
as={StyledTextField}
id="studentCardNumber"
name="studentCardNumber"
label="Diákigazolvány szám"
variant="outlined"
color="primary"
fullWidth
size="small"
/>
</Box>
<Box my={1}>
<Field
as={StyledTextField}
id="roomNumber"
name="roomNumber"
type="number"
label="Szobaszám"
variant="outlined"
color="secondary"
fullWidth
size="small"
/>
</Box>
<Box display="flex" justifyContent="flex-end" mt={3}>
<Button type="submit" variant="contained">
Mentés
</Button>
</Box>
</Box>
</Form>
</Formik>
{redirect && <Redirect to="/" />}
</Container>
);
};
export default ProfileForm;
// eslint-disable-next-line object-curly-newline
import { Avatar, Box, Button, IconButton, TextField, Typography } from '@material-ui/core';
import CloseIcon from '@material-ui/icons/Close';
import React, { Dispatch, SetStateAction } from 'react';
import { CustomModal } from './CustomModal';
type Dispatcher<S> = Dispatch<SetStateAction<S>>;
interface IProfileModalProps {
isOpen: boolean;
setIsOpen: Dispatcher<boolean>;
}
export const ProfileModal: React.FC<IProfileModalProps> = ({ isOpen, setIsOpen }) => {
const onClose = (): void => {
setIsOpen(false);
};
return (
<CustomModal
isOpen={isOpen}
setIsOpen={setIsOpen}
customStyle={{
backgroundColor: '#EBEBEB',
padding: '15px',
width: '500px',
borderRadius: '8px',
}}
>
<Box
fontWeight="fontWeightBold"
display="flex"
justifyContent="space-between"
alignItems="center"
pl={2}
>
<Typography variant="h5" component="h5">
Profilom
</Typography>
<IconButton onClick={onClose}>
<CloseIcon />
</IconButton>
</Box>
<Box display="flex" justifyContent="center" my={3}>
<Avatar alt="Profile image" variant="rounded" style={{ width: '200px', height: '200px' }} />
</Box>
<Box px={1} mt={2} mb={1}>
<Box my={1}>
<TextField
label="Diákigazolvány szám"
variant="outlined"
color="secondary"
fullWidth
size="small"
style={{ backgroundColor: 'white' }}
/>
</Box>
<Box my={1}>
<TextField
label="Szobaszám"
variant="outlined"
color="secondary"
fullWidth
size="small"
style={{ backgroundColor: 'white' }}
/>
</Box>
<Box display="flex" justifyContent="flex-end" mt={3}>
<Button variant="contained" onClick={onClose}>
Mentés
</Button>
</Box>
</Box>
</CustomModal>
);
};
export default ProfileModal;
import { isNil } from 'lodash';
import React, { useEffect } from 'react';
import { Redirect } from 'react-router-dom';
import useMe from '../hooks/useMe';
import { useUserContext } from '../hooks/useUserContext';
interface Props {}
const RememberMe: React.FC = ({ children }) => {
const { setProfile } = useUserContext();
const { data: me } = useMe();
useEffect(() => {
if (me) {
setProfile(me);
} else {
setProfile(undefined);
}
}, [me, setProfile]);
if (isNil(me?.profile) && me?.isLoggedIn) {
return <Redirect to="/register" />;
}
return <>{children}</>;
};
export default RememberMe;
import { TextField, withStyles } from '@material-ui/core';
const StyledTextField = withStyles(({ palette: { primary: { contrastText } } }) => ({
root: {
'& label.Mui-focused': {
color: contrastText,
},
'& label': {
color: contrastText,
},
'& .MuiOutlinedInput-root': {
color: contrastText,
'& fieldset': {
borderColor: contrastText,
},
'&:hover fieldset': {
borderColor: contrastText,
},
'&.Mui-focused fieldset': {
borderColor: contrastText,
},
},
color: contrastText,
},
}))(TextField);
export default StyledTextField;
import React, { createContext, useState } from 'react'; import React, { createContext, useState } from 'react';
import { IProfile, Role } from '../types/Profile'; import { IProfile } from '../types/Profile';
export type ProfileWithStatus = {
profile?: IProfile;
isLoggedIn: boolean;
};
export interface UserContextType { export interface UserContextType {
profile: IProfile | undefined; profile?: ProfileWithStatus;
setProfile: (profile: IProfile) => void; setProfile: (profile: ProfileWithStatus | undefined) => void;
} }
// Context /* // Context
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const initialState: IProfile = { const initialState: IProfile = {
externalId: 'abcd', externalId: 'abcd',
...@@ -17,14 +22,14 @@ const initialState: IProfile = { ...@@ -17,14 +22,14 @@ const initialState: IProfile = {
name: 'Nagy Gizike', name: 'Nagy Gizike',
warnings: [], warnings: [],
notices: [{ _id: '123', text: 'Asd', isSeen: false }], notices: [{ _id: '123', text: 'Asd', isSeen: false }],
}; }; */
export const UserContext = createContext({} as UserContextType); export const UserContext = createContext({} as UserContextType);
const { Provider } = UserContext; const { Provider } = UserContext;
export const UserStateProvider: React.FC = ({ children }) => { export const UserStateProvider: React.FC = ({ children }) => {
const [profile, setProfile] = useState<IProfile | undefined>(initialState); const [profile, setProfile] = useState<ProfileWithStatus>();
return <Provider value={{ profile, setProfile }}>{children}</Provider>; return <Provider value={{ profile, setProfile }}>{children}</Provider>;
}; };
......
import { useMutation } from 'react-query';
import useClientContext from './useClientContext';
export default function useLogin() {
const client = useClientContext();
return useMutation('login', client.client.auth.login);
}
import { useQuery, UseQueryResult } from 'react-query';
import { ProfileWithStatus } from '../context';
import useClientContext from './useClientContext';
export default function useMe(): UseQueryResult<ProfileWithStatus, Error> {
const client = useClientContext();
return useQuery<ProfileWithStatus, Error>('me', async () => client.client.auth.me());
}
import { useMutation, UseMutationResult } from 'react-query';
import { Id, ProfileData } from '../client/auth';
import useClientContext from './useClientContext';
export default function useRegister(): UseMutationResult<Id, Error, ProfileData> {
const client = useClientContext();
return useMutation<any, Error, ProfileData>('register', async (data: ProfileData) =>
client.client.auth.register(data),
);
}
...@@ -20,13 +20,15 @@ const Page: React.FC = ({ children }) => { ...@@ -20,13 +20,15 @@ const Page: React.FC = ({ children }) => {
const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('xs')); const isMobile = useMediaQuery((theme: Theme) => theme.breakpoints.down('xs'));
const classes = useStyles(); const classes = useStyles();
/* const { mutate } = useRegister();
useEffect(() => {
mutate({ roomNumber: 1300, studentCardNumber: '122223455' });
}, [mutate]); */
return ( return (
<Box className={classes.root}> <Box className={classes.root}>
{isMobile ? ( {isMobile ? <MobileHeader onMenuClick={toggleOn} /> : <Header />}
<MobileHeader onMenuClick={toggleOn} />
) : (
<Header handleLogout={(): void => {}} />
)}
{isMobile && ( {isMobile && (
<DrawerMenu open={openDrawer} toggleOpen={toggleOff} handleLogout={(): void => {}} /> <DrawerMenu open={openDrawer} toggleOpen={toggleOff} handleLogout={(): void => {}} />
)} )}
......
import React from 'react';
import ProfileForm from '../components/ProfileForm';
import Page from './Page';
const NewsPage: React.FC = () => (
<Page>
<ProfileForm />
</Page>
);
export default NewsPage;
...@@ -4,7 +4,7 @@ module.exports = function (app) { ...@@ -4,7 +4,7 @@ module.exports = function (app) {
app.use( app.use(
'/api/v1', '/api/v1',
createProxyMiddleware({ createProxyMiddleware({
target: 'https://body-dev.maze.sch.bme.hu/dev', target: process.env.REACT_APP_PROXY_TARGET,
changeOrigin: true, changeOrigin: true,
}), }),
); );
......
...@@ -18,16 +18,10 @@ export interface IWarning { ...@@ -18,16 +18,10 @@ export interface IWarning {
} }
export interface IProfile { export interface IProfile {
externalId: string; _id: string;
studentCardNumber?: string; studentCardNumber?: string;
roomNumber?: Number; roomNumber?: Number;
newPicture?: string;
acceptedPicture?: string;
role: Role; role: Role;
email: string; email: string;
name: string; name: string;
isStaffMember?: boolean;
staffMemberText?: string;
warnings: IWarning[];
notices: INotice[];
} }