Skip to content

Commit

Permalink
Merge pull request #36 from NickSettler/IIS-52-Add-page-for-certain-c…
Browse files Browse the repository at this point in the history
…ourse

feat(courses): add course info page
  • Loading branch information
NickSettler authored Oct 21, 2023
2 parents bc6d943 + 6662eaa commit adaa60a
Show file tree
Hide file tree
Showing 12 changed files with 683 additions and 210 deletions.
17 changes: 10 additions & 7 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,15 @@ const App = (): ReactElement => {

const visibleRoutes = useMemo(
(): Array<TAppRoute> =>
filter(appRoutes, ({ roles, path }) =>
user
? userHasRoles(user, roles ?? null) &&
path !== '/login' &&
path !== '/register'
: path === '/login' || path === '/register',
filter(
filter(appRoutes, ({ roles, path }) =>
user
? userHasRoles(user, roles ?? null) &&
path !== '/login' &&
path !== '/register'
: path === '/login' || path === '/register',
),
({ showInNav }) => showInNav ?? true,
),
[user],
);
Expand Down Expand Up @@ -113,7 +116,7 @@ const App = (): ReactElement => {

<Box component='main' sx={{ flexGrow: 1, px: 3, height: '100vh' }}>
<Toolbar />
<Box sx={{ flexGrow: 1, py: 1, height: 'calc(100% - 64px)' }}>
<Box sx={{ flexGrow: 1, py: 2, height: 'calc(100% - 64px)' }}>
<Routes>
{appRoutes.map(({ path, roles, element }) => (
<Route
Expand Down
6 changes: 6 additions & 0 deletions src/api/courses/course.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export default class CourseService extends BaseService {
return await Api.instance.get<Array<TPureCourse>>(this.endpoint);
}

public static async getCourse(
abbr: TPureCourse[E_COURSE_ENTITY_KEYS.ABBR],
): Promise<TPureCourse> {
return await Api.instance.get<TPureCourse>(`${this.endpoint}/${abbr}`);
}

public static async createCourse(
data: TCreateCourseData,
): Promise<TPureCourse> {
Expand Down
252 changes: 252 additions & 0 deletions src/components/courses/course-info/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
import React, { JSX, useMemo } from 'react';
import { Navigate, useNavigate, useParams } from 'react-router-dom';
import { E_COURSE_ENTITY_KEYS } from '../../../api/courses/types';
import { isUndefined } from 'lodash';
import { useCourse } from '../../../utils/hooks/useCourse';
import {
IconButton,
ListItem,
Paper,
Skeleton,
Stack,
Tooltip,
Typography,
} from '@mui/material';
import Button from '@mui/material/Button';
import { ArrowBack, Delete, Edit } from '@mui/icons-material';
import { E_USER_ENTITY_KEYS } from '../../../api/user/types';
import Grid from '@mui/material/Grid';
import Container from '@mui/material/Container';
import List from '@mui/material/List';
import ListItemText from '@mui/material/ListItemText';
import { useCourseMutations } from '../../../utils/hooks/useCourseMutations';
import { useModal } from '../../../utils/hooks/useModal';
import { E_MODALS } from '../../../store/modals';
import { E_MODAL_MODE } from '../../../utils/modal/base-modal';
import { useCourseModalHandlers } from '../../../utils/hooks/useCourseModalHandlers';
import { toast } from 'react-hot-toast';
import { useCoursePermissions } from '../../../utils/hooks/useCoursePermissions';

export const CourseInfo = (): JSX.Element => {
const { abbr } = useParams<'abbr'>();
const navigate = useNavigate();

if (isUndefined(abbr)) navigate('/courses');

const { onOpen: openCourseFormModal, onClose: closeCourseFormModal } =
useModal(E_MODALS.COURSE_FORM);

const { data, isLoading, error, refetch } = useCourse(abbr);

const { canUpdateCourse, canDeleteCourse } = useCoursePermissions(
data ?? undefined,
);

const { updateMutation, deleteMutation } = useCourseMutations({
refetch,
closeCourseFormModal,
});

const { handleUpdateSuccess } = useCourseModalHandlers({
updateMutation,
});

const handleEditClick = () => {
if (!data) return;

openCourseFormModal({
abbr: data[E_COURSE_ENTITY_KEYS.ABBR],
mode: E_MODAL_MODE.UPDATE,
initialData: data,
onSuccess: handleUpdateSuccess,
});
};

const handleDeleteClick = () => {
if (!data || !abbr) return;

deleteMutation.mutate(
{
[E_COURSE_ENTITY_KEYS.ABBR]: abbr,
},
{
onSuccess: async () => {
navigate(-1);

toast.success('Course deleted successfully');
},
onError: async () => {
await refetch();

toast.error('Failed to delete course');
},
},
);
};

const title = useMemo(
() =>
data
? `${data[E_COURSE_ENTITY_KEYS.ABBR]} - ${
data[E_COURSE_ENTITY_KEYS.NAME]
}`
: '',
[data],
);

const guarantor = useMemo(() => {
if (!data) return '';

const g = data[E_COURSE_ENTITY_KEYS.GUARANTOR];

return `${g[E_USER_ENTITY_KEYS.FIRST_NAME]} ${
g[E_USER_ENTITY_KEYS.LAST_NAME]
} (${g[E_USER_ENTITY_KEYS.USERNAME]})`;
}, [data]);

const credits = useMemo(
() => (data ? data[E_COURSE_ENTITY_KEYS.CREDITS] : ''),
[data],
);

const annotation = useMemo(
() => (data ? data[E_COURSE_ENTITY_KEYS.ANNOTATION] : ''),
[data],
);

const teachers = useMemo(
() => (data ? data[E_COURSE_ENTITY_KEYS.TEACHERS] : []),
[data],
);

if (error && error.statusCode === 404) return <Navigate to={'/courses'} />;

return (
<Stack gap={4}>
<Stack direction='row' justifyContent='space-between' gap={4}>
<Stack direction='row' gap={2} alignItems={'center'}>
<Tooltip title='Back'>
<IconButton onClick={() => navigate(-1)}>
<ArrowBack />
</IconButton>
</Tooltip>
{isLoading ? (
<Skeleton
variant={'rectangular'}
animation={'wave'}
sx={{ maxWidth: 'initial' }}
>
<Typography variant='h4' flexGrow={1}></Typography>
</Skeleton>
) : (
<Typography variant='h4' flexGrow={1}>
{title}
</Typography>
)}
</Stack>
{(canUpdateCourse || canDeleteCourse) && (
<Stack direction='row' gap={1} alignItems={'center'}>
{canUpdateCourse && (
<Button
size={'small'}
variant={'text'}
startIcon={<Edit />}
onClick={handleEditClick}
>
Edit
</Button>
)}
{canDeleteCourse && (
<Button
size={'small'}
variant={'text'}
color={'error'}
startIcon={<Delete />}
onClick={handleDeleteClick}
>
Delete
</Button>
)}
</Stack>
)}
</Stack>
<Container maxWidth={false} disableGutters>
<Grid container columnSpacing={4} direction={'row'}>
<Grid container item xs={6} rowSpacing={1}>
<Grid item xs={12}>
<Typography variant={'h6'}>Guarantor</Typography>
{isLoading ? (
<Skeleton
variant={'rectangular'}
animation={'wave'}
sx={{ maxWidth: 'initial' }}
>
<Typography variant='body1' flexGrow={1}>
Admin
</Typography>
</Skeleton>
) : (
<Typography variant='body1' flexGrow={1}>
{guarantor}
</Typography>
)}
</Grid>
<Grid item xs={12}>
<Typography variant={'h6'}>Credits</Typography>
{isLoading ? (
<Skeleton
variant={'rectangular'}
animation={'wave'}
sx={{ maxWidth: 'initial' }}
>
<Typography variant='body1' flexGrow={1}>
0
</Typography>
</Skeleton>
) : (
<Typography variant='body1' flexGrow={1}>
{credits}
</Typography>
)}
</Grid>
<Grid item xs={12}>
<Typography variant={'h6'}>Annotation</Typography>
{isLoading ? (
<Skeleton
variant={'rectangular'}
animation={'wave'}
sx={{ maxWidth: 'initial' }}
>
<Typography variant='body1' flexGrow={1}>
None
</Typography>
</Skeleton>
) : (
<Typography variant='body1' flexGrow={1}>
{annotation}
</Typography>
)}
</Grid>
</Grid>
<Grid container item direction={'column'} xs={6} rowGap={1}>
<Typography variant={'h6'}>Teachers</Typography>
<Paper variant={'outlined'}>
<List dense disablePadding>
{teachers.map((teacher) => (
<ListItem key={teacher[E_USER_ENTITY_KEYS.ID]}>
<ListItemText
primary={`${teacher[E_USER_ENTITY_KEYS.FIRST_NAME]} ${
teacher[E_USER_ENTITY_KEYS.LAST_NAME]
}`}
secondary={teacher[E_USER_ENTITY_KEYS.USERNAME]}
/>
</ListItem>
))}
</List>
</Paper>
</Grid>
</Grid>
</Container>
</Stack>
);
};
Loading

0 comments on commit adaa60a

Please sign in to comment.