Merge pull request #20 from leomurca/feature/professor_classroom

Feature/professor classroom
This commit is contained in:
Leonardo Murça 2023-02-07 20:40:41 -03:00 committed by GitHub
commit 8eca8b79ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
51 changed files with 2962 additions and 313 deletions

View file

@ -1,22 +1,20 @@
import { Navigate, Route, Routes, useNavigate } from 'react-router-dom'; import { lazy } from 'react';
import { useNavigate } from 'react-router-dom';
import { Container } from '@mui/system'; import { Container } from '@mui/system';
import { useUser } from '../context/user';
import { useAuthState } from '../context/auth'; import { useAuthState } from '../context/auth';
import { useUser } from '../context/user';
import MainMenu from '../components/MainMenu'; import MainMenu from '../components/MainMenu';
import Home from '../screens/Home';
import Information from '../screens/Information';
import Calendar from '../screens/Calendar';
import useLayoutType from '../hooks/useLayoutType'; import useLayoutType from '../hooks/useLayoutType';
import Toolbar from '../components/Toolbar'; import Toolbar from '../components/Toolbar';
import Classroom from '../screens/Classroom';
import Assignment from '../screens/Assignment';
import Profile from '../screens/Profile';
import { avatarMenuOptions, menuOptions } from './data'; import { avatarMenuOptions, menuOptions } from './data';
import styles from './styles'; import styles from './styles';
const StudentRoutes = lazy(() => import('./StudentRoutes'));
const ProfessorRoutes = lazy(() => import('./ProfessorRoutes'));
function AuthenticatedApp() { function AuthenticatedApp() {
const navigate = useNavigate(); const navigate = useNavigate();
const { state } = useUser(); const { state } = useUser();
@ -24,6 +22,17 @@ function AuthenticatedApp() {
const layoutType = useLayoutType(); const layoutType = useLayoutType();
const { container, toolbar } = styles[layoutType]; const { container, toolbar } = styles[layoutType];
const routeResolver = role => {
switch (role) {
case 'STUDENT':
return <StudentRoutes />;
case 'PROFESSOR':
return <ProfessorRoutes />;
default:
return null;
}
};
return ( return (
state && state &&
state.user && ( state.user && (
@ -43,21 +52,7 @@ function AuthenticatedApp() {
options={menuOptions(state.pathname)} options={menuOptions(state.pathname)}
layoutType={layoutType} layoutType={layoutType}
/> />
<Routes> {routeResolver(state.user.role)}
<Route path="/home" element={<Home />} />
<Route path="/info" element={<Information />} />
<Route path="/calendar" element={<Calendar />} />
<Route path="/profile" element={<Profile />} />
<Route path="/class">
<Route path=":id" element={<Classroom />} />
</Route>
<Route path="/assignment">
<Route path=":id" element={<Assignment />} />
</Route>
<Route path="/login" element={<Navigate to="/home" />} />
<Route path="/register" element={<Navigate to="/home" />} />
<Route path="/" element={<Navigate to="/home" />} />
</Routes>
</Container> </Container>
</> </>
) )

View file

@ -0,0 +1,22 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import Classroom from '../screens/professor/Classroom';
import Home from '../screens/professor/Home';
function ProfessorRoutes() {
return (
<Routes>
<Route path="/calendar" element={<h1>Calendar</h1>} />
<Route path="/profile" element={<h1>Profile</h1>} />
<Route path="/class">
<Route path=":id" element={<Classroom />} />
</Route>
<Route path="/info" element={<h1>Information</h1>} />
<Route path="/home" element={<Home />} />
<Route path="/login" element={<Navigate to="/home" />} />
<Route path="/register" element={<Navigate to="/home" />} />
<Route path="/" element={<Navigate to="/home" />} />
</Routes>
);
}
export default ProfessorRoutes;

31
src/app/StudentRoutes.js Normal file
View file

@ -0,0 +1,31 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import Home from '../screens/student/Home';
import Classroom from '../screens/student/Classroom';
import Information from '../screens/Information';
import Calendar from '../screens/Calendar';
import Assignment from '../screens/Assignment';
import Profile from '../screens/Profile';
function StudentRoutes() {
return (
<Routes>
<Route path="/home" element={<Home />} />
<Route path="/info" element={<Information />} />
<Route path="/calendar" element={<Calendar />} />
<Route path="/profile" element={<Profile />} />
<Route path="/class">
<Route path=":id" element={<Classroom />} />
</Route>
<Route path="/assignment">
<Route path=":id" element={<Assignment />} />
</Route>
<Route path="/login" element={<Navigate to="/home" />} />
<Route path="/register" element={<Navigate to="/home" />} />
<Route path="/" element={<Navigate to="/home" />} />
</Routes>
);
}
export default StudentRoutes;

View file

@ -28,6 +28,7 @@ const menuOptions = activePath => [
isActive: isActive:
activePath === '/home' || activePath === '/home' ||
activePath === '/login' || activePath === '/login' ||
activePath === '/register' ||
activePath === '/profile' || activePath === '/profile' ||
activePath === '/' || activePath === '/' ||
activePath.indexOf('class') !== -1 || activePath.indexOf('class') !== -1 ||

View file

@ -18,6 +18,10 @@ function AssignmentCard({
classrooms, classrooms,
dueDate, dueDate,
scores, scores,
deliveredByStudents,
reviewed,
total,
isAssignmentToReview,
layoutType, layoutType,
onClick, onClick,
}) { }) {
@ -61,16 +65,30 @@ function AssignmentCard({
{classrooms.map(c => c.name).join(', ')} {classrooms.map(c => c.name).join(', ')}
</Typography> </Typography>
<Divider sx={dividerMiddle} /> <Divider sx={dividerMiddle} />
<Typography sx={typographyDueDate} variant="p" component="div"> <Typography sx={typographyDueDate} variant="p" component="div">
<strong>Data de entrega: </strong>{' '} <strong>Data de entrega: </strong>{' '}
{capitalizeFirstLetter( {capitalizeFirstLetter(
dayjs(dueDate).format('dddd, DD/MM | HH:mm[h]') dayjs(dueDate).format('dddd, DD/MM | HH:mm[h]')
)} )}
</Typography> </Typography>
<Typography variant="p" component="div"> {deliveredByStudents >= 0 && total && (
<strong>Valor: </strong> <Typography variant="p" component="div">
{scores.map(s => s.value).join(', ')} pts <strong>Entregues: </strong>{' '}
</Typography> {`${deliveredByStudents} de ${total}`}
</Typography>
)}
{reviewed >= 0 && total && (
<Typography variant="p" component="div">
<strong>Corrigidas: </strong> {`${reviewed} de ${total}`}
</Typography>
)}
{!isAssignmentToReview && (
<Typography variant="p" component="div">
<strong>Valor: </strong>
{scores.map(s => s.value).join(', ')} pts
</Typography>
)}
</Stack> </Stack>
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
@ -110,10 +128,23 @@ function AssignmentCard({
dayjs(dueDate).format('dddd, DD/MM | HH:mm[h]') dayjs(dueDate).format('dddd, DD/MM | HH:mm[h]')
)} )}
</Typography> </Typography>
<Typography variant="p" component="div"> {deliveredByStudents >= 0 && total && (
<strong>Valor: </strong> <Typography variant="p" component="div">
{scores.map(s => s.value).join(', ')} pts <strong>Entregues: </strong>{' '}
</Typography> {`${deliveredByStudents} de ${total}`}
</Typography>
)}
{reviewed >= 0 && total && (
<Typography variant="p" component="div">
<strong>Corrigidas: </strong> {`${reviewed} de ${total}`}
</Typography>
)}
{!isAssignmentToReview && (
<Typography variant="p" component="div">
<strong>Valor: </strong>
{scores.map(s => s.value).join(', ')} pts
</Typography>
)}
</Stack> </Stack>
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>

View file

@ -17,6 +17,7 @@ function ClassCard({
title, title,
color, color,
teachers, teachers,
course,
layoutType, layoutType,
onClick, onClick,
}) { }) {
@ -45,23 +46,30 @@ function ClassCard({
> >
{title} {title}
</Typography> </Typography>
<Stack alignItems="center" direction="row" spacing={1}> {teachers && (
<AvatarGroup total={teachers.length}> <Stack alignItems="center" direction="row" spacing={1}>
{teachers.map(t => ( <AvatarGroup total={teachers.length}>
<Avatar {teachers.map(t => (
key={t.name} <Avatar
alt={t.name} key={t.name}
src={t.avatar} alt={t.name}
sx={avatar} src={t.avatar}
/> sx={avatar}
))} />
</AvatarGroup> ))}
<Tooltip title={teachers.map(t => t.name).join(', ')}> </AvatarGroup>
<Typography sx={tooltip} variant="body3" color="text.secondary"> <Tooltip title={teachers.map(t => t.name).join(', ')}>
{teachers.map(t => t.name).join(', ')} <Typography
</Typography> sx={tooltip}
</Tooltip> variant="body3"
</Stack> color="text.secondary"
>
{teachers.map(t => t.name).join(', ')}
</Typography>
</Tooltip>
</Stack>
)}
{course && <Typography variant="body2">{course}</Typography>}
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
</Card> </Card>
@ -82,21 +90,24 @@ function ClassCard({
> >
{title} {title}
</Typography> </Typography>
<Stack alignItems="center" direction="row" spacing={1}> {teachers && (
<AvatarGroup total={teachers.length}> <Stack alignItems="center" direction="row" spacing={1}>
{teachers.map(t => ( <AvatarGroup total={teachers.length}>
<Avatar {teachers.map(t => (
key={t.name} <Avatar
alt={t.name} key={t.name}
src={t.avatar} alt={t.name}
sx={avatar} src={t.avatar}
/> sx={avatar}
))} />
</AvatarGroup> ))}
<Typography sx={tooltip} variant="body2" color="text.secondary"> </AvatarGroup>
{teachers.map(t => t.name).join(', ')} <Typography sx={tooltip} variant="body2" color="text.secondary">
</Typography> {teachers.map(t => t.name).join(', ')}
</Stack> </Typography>
</Stack>
)}
{course && <Typography variant="body2">{course}</Typography>}
</CardContent> </CardContent>
</CardActionArea> </CardActionArea>
</Card> </Card>

View file

@ -0,0 +1,33 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from '@mui/material';
function FormDialog({
isOpened,
title,
contentText,
inputs,
onDismiss,
onSave,
}) {
return (
<Dialog open={isOpened} onClose={onDismiss}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText>{contentText}</DialogContentText>
{[...inputs]}
</DialogContent>
<DialogActions>
<Button onClick={onDismiss}>Cancelar</Button>
<Button onClick={onSave}>Salvar</Button>
</DialogActions>
</Dialog>
);
}
export default FormDialog;

View file

@ -0,0 +1,64 @@
import {
Avatar,
Button,
Card,
Stack,
TextField,
Typography,
} from '@mui/material';
import { useState } from 'react';
import styles from './styles';
function PublishAnnouncementCard({ layoutType, user, value, onChange }) {
const [isComposing, setIsComposing] = useState(false);
const { card, publishAnnouncement } = styles[layoutType];
return (
<Card sx={card} elevation={4} variant="elevation">
{isComposing ? (
<Stack
sx={publishAnnouncement}
alignItems="end"
direction="column"
spacing={2}
>
<TextField
value={value}
onChange={onChange}
sx={{ width: '100%' }}
id="outlined-multiline-static"
label="Escreva um comunicado para sua turma"
autoFocus
multiline
minRows={4}
/>
<Stack direction="row" spacing={4}>
<Button onClick={() => setIsComposing(false)} variant="text">
Cancelar
</Button>
<Button onClick={() => console.log('clicked')} variant="contained">
Postar
</Button>
</Stack>
</Stack>
) : (
<Stack
sx={publishAnnouncement}
alignItems="center"
direction="row"
spacing={2}
onClick={() => setIsComposing(true)}
>
<Avatar alt={user.firstName} src={user.avatar} />
<Typography id="outlined-multiline-static" sx={{ width: '100%' }}>
Escreva um comunicado para sua turma
</Typography>
</Stack>
)}
</Card>
);
}
export default PublishAnnouncementCard;

View file

@ -0,0 +1,47 @@
// ========== Desktop ==========
const desktopCard = {
width: '100%',
padding: '20px',
};
const desktopPublishAnnouncement = {
cursor: 'pointer',
':hover': {
color: '#32A041',
},
};
const desktop = {
publishAnnouncement: desktopPublishAnnouncement,
card: desktopCard,
};
// ========== Mobile ==========
const mobilePublishAnnouncement = {
cursor: 'pointer',
padding: '10px',
width: '100%',
':hover': {
color: '#32A041',
},
};
const mobileCard = {
width: '100%',
padding: '10px ',
};
const mobile = {
publishAnnouncement: mobilePublishAnnouncement,
card: mobileCard,
};
// ========== Unset ==========
const unset = {
publishAnnouncement: null,
card: null,
};
const styles = { desktop, mobile, unset };
export default styles;

View file

@ -1,5 +1,5 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { getUser, registerUser } from '../services/user-service'; import { CommonApi } from '../utils/mocks/api';
const AuthContext = createContext(); const AuthContext = createContext();
@ -22,10 +22,9 @@ function AuthProvider(props) {
const register = data => { const register = data => {
setState({ ...state, status: 'pending' }); setState({ ...state, status: 'pending' });
let shouldFail = false;
return registerUser(data, shouldFail).then(data => { return CommonApi.registerUser(data).then(data => {
if (shouldFail) { if (data.message) {
return setState({ status: 'error', user: null, error: data }); return setState({ status: 'error', user: null, error: data });
} else { } else {
return setState({ status: 'success', user: data, error: null }); return setState({ status: 'success', user: data, error: null });
@ -35,10 +34,9 @@ function AuthProvider(props) {
const login = (email, password) => { const login = (email, password) => {
setState({ ...state, status: 'pending' }); setState({ ...state, status: 'pending' });
let shouldFail = email !== 'teste@teste.com' || password !== '#teste1234';
return getUser(shouldFail).then(data => { return CommonApi.getUser(email, password).then(data => {
if (shouldFail) { if (data.message) {
return setState({ status: 'error', user: null, error: data }); return setState({ status: 'error', user: null, error: data });
} else { } else {
return setState({ status: 'success', user: data, error: null }); return setState({ status: 'success', user: data, error: null });

View file

@ -1,23 +1,14 @@
import { createContext, useContext, useEffect, useState } from 'react'; import { createContext, useContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import { UserServiceProvider } from '../services/provider';
import { useAuthState } from './auth'; import { useAuthState } from './auth';
import {
getAllAssignments,
getAssignmentById,
getAssignmentsByClassId,
getClassroomAnnouncementsById,
getClassroomById,
getClassrooms,
getFaq,
getPeopleByClassId,
getUpcomingAssignmentsByClassId,
} from '../services/user-service';
const UserContext = createContext(); const UserContext = createContext();
function UserProvider(props) { function UserProvider(props) {
const { user } = useAuthState(); const { user } = useAuthState();
const { pathname } = useLocation(); const { pathname } = useLocation();
const [userService, setUserService] = useState(null);
const [state, setState] = useState({ const [state, setState] = useState({
user: null, user: null,
error: null, error: null,
@ -26,73 +17,23 @@ function UserProvider(props) {
useEffect(() => { useEffect(() => {
setState({ user, pathname }); setState({ user, pathname });
async function initUserService() {
if (user) {
const instance = await UserServiceProvider.getInstance(user);
setUserService(instance);
}
}
initUserService();
}, [user, pathname]); }, [user, pathname]);
const fetchClassrooms = () => getClassrooms(user.id); return <UserContext.Provider value={{ state, userService }} {...props} />;
const fetchAllAssignments = () => getAllAssignments(user.id);
const fetchAssignmentById = assignmentId => getAssignmentById(assignmentId);
const fetchAssignmentsByClassId = classId => getAssignmentsByClassId(classId);
const fetchClassroomById = classId => getClassroomById(classId);
const fetchFAQ = () => getFaq();
const fetchClassroomAnnouncements = classId =>
getClassroomAnnouncementsById(classId);
const fetchUpcomingAssignmentsByClassId = classId =>
getUpcomingAssignmentsByClassId(classId);
const fetchPeopleByClassId = classId => getPeopleByClassId(classId);
return (
<UserContext.Provider
value={{
state,
fetchClassrooms,
fetchAllAssignments,
fetchAssignmentById,
fetchAssignmentsByClassId,
fetchClassroomById,
fetchFAQ,
fetchClassroomAnnouncements,
fetchUpcomingAssignmentsByClassId,
fetchPeopleByClassId,
}}
{...props}
/>
);
} }
function useUser() { function useUser() {
const { const { state, userService } = useContext(UserContext);
state,
fetchClassrooms,
fetchAssignmentById,
fetchAllAssignments,
fetchAssignmentsByClassId,
fetchClassroomById,
fetchFAQ,
fetchClassroomAnnouncements,
fetchUpcomingAssignmentsByClassId,
fetchPeopleByClassId,
} = useContext(UserContext);
return { return { state, userService };
state,
fetchClassrooms,
fetchAllAssignments,
fetchAssignmentById,
fetchAssignmentsByClassId,
fetchClassroomById,
fetchFAQ,
fetchClassroomAnnouncements,
fetchUpcomingAssignmentsByClassId,
fetchPeopleByClassId,
};
} }
export { UserProvider, useUser }; export { UserProvider, useUser };

View file

@ -8,14 +8,14 @@ import View from './View';
function Assignment() { function Assignment() {
const params = useParams(); const params = useParams();
const layoutType = useLayoutType(); const layoutType = useLayoutType();
const { fetchAssignmentById } = useUser(); const { userService } = useUser();
const [assignment, setAssignment] = useState(null); const [assignment, setAssignment] = useState(null);
const dropzone = useDropzone({ maxFiles: 5 }); const dropzone = useDropzone({ maxFiles: 5 });
useEffect(() => { useEffect(() => {
async function getAssignmentById(assignmentId) { async function getAssignmentById(assignmentId) {
document.title = 'Carregando...'; document.title = 'Carregando...';
const result = await fetchAssignmentById(assignmentId); const result = await userService.fetchAssignmentById(assignmentId);
setAssignment(result.data); setAssignment(result.data);
} }
@ -27,7 +27,7 @@ function Assignment() {
getAssignmentById(params.id); getAssignmentById(params.id);
updateDocumentTitle(); updateDocumentTitle();
}, [params, fetchAssignmentById, assignment]); }, [params, userService, userService.fetchAssignmentById, assignment]);
return ( return (
<View assignment={assignment} dropzone={dropzone} layoutType={layoutType} /> <View assignment={assignment} dropzone={dropzone} layoutType={layoutType} />

View file

@ -9,16 +9,16 @@ import { sectors } from './data';
function Information() { function Information() {
useDocumentTitle('Informações'); useDocumentTitle('Informações');
const layoutType = useLayoutType(); const layoutType = useLayoutType();
const { fetchFAQ } = useUser(); const { userService } = useUser();
const [faq, setFaq] = useState(null); const [faq, setFaq] = useState(null);
useEffect(() => { useEffect(() => {
async function getClassrooms() { async function getClassrooms() {
const result = await fetchFAQ(); const result = await userService.fetchFAQ();
setFaq(result.data); setFaq(result.data);
} }
getClassrooms(); getClassrooms();
}, [fetchFAQ]); }, [userService, userService.fetchFAQ]);
return <View faq={faq} sectors={sectors} layoutType={layoutType} />; return <View faq={faq} sectors={sectors} layoutType={layoutType} />;
} }

View file

@ -0,0 +1,525 @@
import { useState } from 'react';
import {
Button,
Card,
Container,
Grid,
IconButton,
Link,
Menu,
MenuItem,
Skeleton,
Stack,
TextField,
Tooltip,
Typography,
} from '@mui/material';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import AnnouncementCard from '../../../../components/AnnouncementCard';
import PublishAnnouncementCard from '../../../../components/PublishAnnouncementCard';
import FormDialog from '../../../../components/FormDialog';
import styles from './styles';
import jitsiLogo from '../../../../assets/jitsi.svg';
import { createArrayFrom1ToN } from '../../../../utils/createArrayFrom1ToN';
function AnnouncementsTab({
layoutType,
announcementsTabData,
classroom,
onChangeEditInput,
onSaveEditChanges,
user,
}) {
const [anchorEl, setAnchorEl] = useState({
virtualRoom: null,
appointmentSlots: null,
});
const [dialogOpened, setDialogOpened] = useState(null);
const [composingTextValue, setComposingTextValue] = useState('');
const { container, emptyStateContainer } = styles[layoutType];
const onSaveEdit = anchorName => {
onSaveEditChanges();
setDialogOpened(null);
setAnchorEl({ ...anchorEl, [anchorName]: null });
};
const onDismissEdit = anchorName => {
setDialogOpened(null);
setAnchorEl({ ...anchorEl, [anchorName]: null });
};
const layoutResolver = (state, layoutType) => {
if (layoutType === 'desktop') {
switch (state) {
case 'loading':
return (
<Grid sx={container} container spacing={2}>
<Grid sx={{ padding: '0 !important' }} item xs={4}>
{createArrayFrom1ToN(3).map(i => (
<Skeleton
key={i}
variant="rectangular"
width="100%"
height={200}
sx={{ marginBottom: '30px' }}
/>
))}
</Grid>
<Grid sx={{ paddingTop: '0 !important' }} item xs={8}>
{createArrayFrom1ToN(4).map(i => (
<Skeleton
key={i}
variant="rectangular"
width="100%"
height={250}
sx={{ marginBottom: '30px' }}
/>
))}
</Grid>
</Grid>
);
case 'idle':
return (
<Grid sx={container} container spacing={2}>
<Grid sx={{ padding: '0 !important' }} item xs={4}>
<Stack gap="30px">
<Card
sx={{ width: '100%', padding: '20px', paddingTop: '10px' }}
elevation={4}
variant="elevation"
>
<Stack justifyContent="flex-start" spacing={1}>
<Container
disableGutters
sx={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<Stack direction="row">
<img src={jitsiLogo} alt="Jitsi Meet" />
<h3 style={{ fontWeight: 500 }}>
Sala de aula virtual
</h3>
</Stack>
<Tooltip title="Opcoes">
<IconButton
onClick={e =>
setAnchorEl({
...anchorEl,
virtualRoom: e.currentTarget,
})
}
aria-label="edit"
size="medium"
>
<MoreVertIcon fontSize="inherit" />
</IconButton>
</Tooltip>
<Menu
id="menu-appbar-virtual-room"
anchorEl={anchorEl.virtualRoom}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorEl.virtualRoom)}
onClose={() =>
setAnchorEl({ ...anchorEl, virtualRoom: null })
}
>
<MenuItem
onClick={() => setDialogOpened('virtualRoom')}
>
<Typography textAlign="center">Editar</Typography>
</MenuItem>
</Menu>
<FormDialog
isOpened={dialogOpened === 'virtualRoom'}
title="Alterar url da sala de aula virtual"
contentText="Edite o campo abaixo para alterar a url da sua sala de aula virtual."
inputs={[
<TextField
autoFocus
margin="dense"
name="virtualRoom"
type="text"
value={classroom.virtualRoom}
onChange={onChangeEditInput}
fullWidth
variant="standard"
/>,
]}
onDismiss={() => onDismissEdit('virtualRoom')}
onSave={() => onSaveEdit('virtualRoom')}
/>
</Container>
<Button
sx={{ marginTop: '15px' }}
variant="contained"
href={classroom.virtualRoom}
target="__blank"
>
Iniciar aula
</Button>
</Stack>
</Card>
<Card
sx={{ width: '100%', padding: '20px', paddingTop: '10px' }}
elevation={4}
variant="elevation"
>
<Stack justifyContent="flex-start" spacing={1}>
<h3 style={{ fontWeight: 500 }}>Próximas Atividades</h3>
{announcementsTabData.upcomingAssignments.length !== 0 ? (
announcementsTabData.upcomingAssignments.map(ua => (
<Link
href={`/assignment/${ua.id}`}
sx={{ fontSize: '15px' }}
key={ua.id}
>
{ua.title}
</Link>
))
) : (
<Container disableGutters>
<p>Nenhuma atividade encontrada!</p>
</Container>
)}
</Stack>
</Card>
<Card
sx={{ width: '100%', padding: '20px', paddingTop: '10px' }}
elevation={4}
variant="elevation"
>
<Stack justifyContent="flex-start" spacing={1}>
<Container
disableGutters
sx={{
display: 'flex',
justifyContent: 'space-between',
}}
>
<h3 style={{ fontWeight: 500 }}>
Horários de Atendimento
</h3>
<Tooltip title="Opcoes">
<IconButton
onClick={e =>
setAnchorEl({
...anchorEl,
appointmentSlots: e.currentTarget,
})
}
aria-label="edit"
size="medium"
>
<MoreVertIcon fontSize="inherit" />
</IconButton>
</Tooltip>
<Menu
id="menu-appbar-appointment-slots"
anchorEl={anchorEl.appointmentSlots}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorEl.appointmentSlots)}
onClose={() =>
setAnchorEl({ ...anchorEl, appointmentSlots: null })
}
>
<MenuItem
onClick={() => setDialogOpened('appointmentSlots')}
>
<Typography textAlign="center">Editar</Typography>
</MenuItem>
</Menu>
<FormDialog
isOpened={dialogOpened === 'appointmentSlots'}
title="Alterar horarios de atendimento"
contentText="Edite os campos abaixo para alterar os horarios de atendimento da disciplina."
inputs={[
classroom.appointmentSlots.map((appts, index) => (
<TextField
key={index}
autoFocus
margin="dense"
name={index}
type="text"
value={`${appts.weekDay}, ${appts.start}h - ${appts.end}h`}
onChange={onChangeEditInput}
fullWidth
variant="standard"
/>
)),
]}
onDismiss={() => onDismissEdit('appointmentSlots')}
onSave={() => onSaveEdit('appointmentSlots')}
/>
</Container>
{classroom.appointmentSlots.map((appts, index) => (
<Typography key={index} variant="body1">
{appts.weekDay}, {appts.start}h - {appts.end}h
</Typography>
))}
</Stack>
</Card>
</Stack>
</Grid>
<Grid sx={{ paddingTop: '0 !important' }} item xs={8}>
<Stack
sx={{ width: '100%', paddingTop: 0 }}
alignItems="center"
justifyContent="center"
flexWrap="wrap"
direction="row"
gap="30px"
>
<PublishAnnouncementCard
layoutType={layoutType}
user={user}
value={composingTextValue}
onChange={e => setComposingTextValue(e.target.value)}
/>
{announcementsTabData.announcements.length !== 0 ? (
announcementsTabData.announcements.map(announcement => (
<AnnouncementCard
key={announcement.id}
announcement={announcement}
/>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum comunicado encontrado!</p>
</Container>
)}
</Stack>
</Grid>
</Grid>
);
case 'gone':
return null;
default:
return null;
}
} else if (layoutType === 'mobile') {
switch (state) {
case 'loading':
return (
<Stack
alignItems="center"
justifyContent="center"
flexWrap="wrap"
direction="row"
gap="30px"
sx={{ marginTop: '30px' }}
>
{createArrayFrom1ToN(3).map(i => (
<Skeleton
key={i}
variant="rectangular"
width="100%"
height={200}
sx={{ marginBottom: '30px' }}
/>
))}
{createArrayFrom1ToN(4).map(i => (
<Skeleton
key={i}
variant="rectangular"
width="100%"
height={250}
sx={{ marginBottom: '30px' }}
/>
))}
</Stack>
);
case 'idle':
return (
<Stack
alignItems="center"
justifyContent="center"
flexWrap="wrap"
direction="row"
gap="30px"
sx={{ marginTop: '30px' }}
>
<Stack gap="30px" sx={{ width: '100%' }}>
<Card
sx={{ width: '100%', padding: '20px', paddingTop: '10px' }}
elevation={4}
variant="elevation"
>
<Stack justifyContent="flex-start" spacing={1}>
<Container
disableGutters
sx={{ display: 'flex', justifyContent: 'space-between' }}
>
<Stack direction="row">
<img src={jitsiLogo} alt="Jitsi Meet" />
<h3 style={{ fontWeight: 500 }}>
Sala de aula virtual
</h3>
</Stack>
<Tooltip title="Opcoes">
<IconButton
onClick={e => setAnchorEl(e.currentTarget)}
aria-label="edit"
size="medium"
>
<MoreVertIcon fontSize="inherit" />
</IconButton>
</Tooltip>
<Menu
id="menu-appbar"
anchorEl={anchorEl}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right',
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right',
}}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
>
<MenuItem
onClick={() => setDialogOpened('virtualRoom')}
>
<Typography textAlign="center">Editar</Typography>
</MenuItem>
</Menu>
<FormDialog
isOpened={dialogOpened === 'virtualRoom'}
title="Alterar url da sala de aula virtual"
contentText="Edite o campo abaixo para alterar a url da sua sala de aula virtual."
inputs={[
<TextField
autoFocus
margin="dense"
name="virtualRoom"
type="text"
value={classroom.virtualRoom}
onChange={onChangeEditInput}
fullWidth
variant="standard"
/>,
]}
onDismiss={() => onDismissEdit('virtualRoom')}
onSave={() => onSaveEdit('virtualRoom')}
/>
</Container>
<Button
variant="contained"
href={classroom.virtualRoom}
target="__blank"
>
Iniciar aula
</Button>
</Stack>
</Card>
<Card
sx={{ width: '100%', padding: '20px', paddingTop: '10px' }}
elevation={4}
variant="elevation"
>
<Stack justifyContent="flex-start" spacing={1}>
<h3 style={{ fontWeight: 500 }}>Próximas Atividades</h3>
{announcementsTabData.upcomingAssignments.length !== 0 ? (
announcementsTabData.upcomingAssignments.map(ua => (
<Link
href={`/assignment/${ua.id}`}
sx={{ fontSize: '15px' }}
key={ua.id}
>
{ua.title}
</Link>
))
) : (
<Container disableGutters>
<p>Nenhuma atividade encontrada!</p>
</Container>
)}
</Stack>
</Card>
<Card
sx={{ width: '100%', padding: '20px', paddingTop: '10px' }}
elevation={4}
variant="elevation"
>
<Stack justifyContent="flex-start" spacing={1}>
<h3 style={{ fontWeight: 500 }}>Horários de Atendimento</h3>
{classroom.appointmentSlots.map((appts, index) => (
<Typography key={index} variant="body1">
{appts.weekDay}, {appts.start}h - {appts.end}h
</Typography>
))}
</Stack>
</Card>
</Stack>
<Stack
sx={{ width: '100%' }}
alignItems="center"
justifyContent="center"
flexWrap="wrap"
direction="row"
gap="30px"
>
<PublishAnnouncementCard
layoutType={layoutType}
user={user}
value={composingTextValue}
onChange={e => setComposingTextValue(e.target.value)}
/>
{announcementsTabData.announcements.length !== 0 ? (
announcementsTabData.announcements.map(announcement => (
<AnnouncementCard
key={announcement.id}
announcement={announcement}
/>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum comunicado encontrado!</p>
</Container>
)}
</Stack>
</Stack>
);
case 'gone':
return null;
default:
return null;
}
}
};
return layoutResolver(
announcementsTabData && announcementsTabData.state,
layoutType
);
}
export default AnnouncementsTab;

View file

@ -0,0 +1,49 @@
// ========== Desktop ==========
const desktopContainer = {
width: '100%',
height: '100vh',
backgroundColor: '#red',
padding: 0,
margin: 0,
marginTop: '50px',
};
const desktopEmptyStateContainer = {
display: 'flex',
justifyContent: 'center',
marginTop: '30px',
};
const desktop = {
container: desktopContainer,
emptyStateContainer: desktopEmptyStateContainer,
};
// ========== Mobile ==========
const mobileContainer = {
width: '90%',
backgroundColor: '#red',
padding: 0,
marginTop: '30px',
paddingBottom: '100px',
};
const mobileEmptyStateContainer = {
display: 'flex',
justifyContent: 'center',
marginTop: '30px',
};
const mobile = {
container: mobileContainer,
emptyStateContainer: mobileEmptyStateContainer,
};
// ========== Unset ==========
const unset = {
container: null,
emptyStateContainer: null,
};
const styles = { desktop, mobile, unset };
export default styles;

View file

@ -0,0 +1,409 @@
import {
Container,
Fab,
Link,
Skeleton,
Stack,
Typography,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import dayjs from 'dayjs';
import { capitalizeFirstLetter } from '../../../../utils/capitalizeFirstLetter';
import styles from './styles';
function AssignmentsTab({ assignmentsTabData, layoutType }) {
const layoutResolver = (state, assignments, layoutType) => {
const {
externalContainer,
innerContainer,
sectionTitle,
assignmentContainer,
assignmentTypography,
assignmentLink,
assignmentDueDate,
assignmentScores,
emptyStateContainer,
} = styles[layoutType];
if (layoutType === 'desktop') {
switch (state) {
case 'loading':
return (
<Container
sx={{
...externalContainer,
display: 'block',
}}
disableGutters
>
<Stack alignItems="center">
<Skeleton
variant="rectangular"
width="90%"
height={70}
sx={{ marginBottom: '30px' }}
/>
<Stack alignItems="flex-start" sx={{ width: '90%' }}>
<Skeleton variant="rectangular" height={50} width="95%" />
<Skeleton
variant="rectangular"
height={20}
width={450}
sx={{ marginTop: '25px' }}
/>
<Skeleton
variant="rectangular"
height={20}
width={300}
sx={{ marginTop: '15px' }}
/>
</Stack>
<Stack
alignItems="flex-start"
sx={{ width: '90%', marginTop: '30px' }}
>
<Skeleton variant="rectangular" height={50} width="95%" />
<Skeleton
variant="rectangular"
height={20}
width={450}
sx={{ marginTop: '25px' }}
/>
<Skeleton
variant="rectangular"
height={20}
width={300}
sx={{ marginTop: '15px' }}
/>
</Stack>
</Stack>
<Stack sx={{ marginTop: '50px' }} alignItems="center">
<Skeleton
variant="rectangular"
width="90%"
height={70}
sx={{ marginBottom: '30px' }}
/>
<Stack alignItems="flex-start" sx={{ width: '90%' }}>
<Skeleton variant="rectangular" height={50} width="95%" />
<Skeleton
variant="rectangular"
height={20}
width={450}
sx={{ marginTop: '25px' }}
/>
<Skeleton
variant="rectangular"
height={20}
width={300}
sx={{ marginTop: '15px' }}
/>
</Stack>
</Stack>
</Container>
);
case 'idle':
const assesments = assignments.filter(a => a.type === 'assessment');
const projects = assignments.filter(a => a.type === 'project');
return (
<Container sx={externalContainer} disableGutters>
<Fab
sx={{ width: 'fit-content', marginRight: '5%' }}
color="primary"
aria-label="add"
variant="extended"
>
<AddIcon />
Criar atividade
</Fab>
<Container sx={innerContainer} disableGutters>
<Typography sx={sectionTitle} variant="h4">
Provas
</Typography>
<Stack alignItems="center">
{assesments.length !== 0 ? (
assesments.map(a => (
<Container
key={a.id}
sx={assignmentContainer}
disableGutters
>
<Typography variant="body1" sx={assignmentTypography}>
<Link
sx={assignmentLink}
href={`/assignment/${a.id}`}
>
{a.title}
</Link>
</Typography>
<Typography
sx={assignmentDueDate}
variant="p"
component="div"
>
<strong>Data de entrega: </strong>{' '}
{capitalizeFirstLetter(
dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]')
)}
</Typography>
<Typography
sx={assignmentScores}
variant="p"
component="div"
>
<strong>Valor: </strong>
{a.scores.map(s => s.value).join(', ')} pts
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhuma prova encontrada!</p>
</Container>
)}
</Stack>
</Container>
<Container sx={innerContainer} disableGutters>
<Typography sx={sectionTitle} variant="h4">
Trabalhos
</Typography>
<Stack alignItems="center">
{projects.length !== 0 ? (
projects.map(a => (
<Container
key={a.id}
sx={assignmentContainer}
disableGutters
>
<Typography variant="body1" sx={assignmentTypography}>
<Link
sx={assignmentLink}
href={`/assignment/${a.id}`}
>
{a.title}
</Link>
</Typography>
<Typography
sx={assignmentDueDate}
variant="p"
component="div"
>
<strong>Data de entrega: </strong>{' '}
{capitalizeFirstLetter(
dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]')
)}
</Typography>
<Typography
sx={assignmentScores}
variant="p"
component="div"
>
<strong>Valor: </strong>
{a.scores.map(s => s.value).join(', ')} pts
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum trabalho encontrado!</p>
</Container>
)}
</Stack>
</Container>
</Container>
);
case 'gone':
return null;
default:
return null;
}
} else if (layoutType === 'mobile') {
switch (state) {
case 'loading':
return (
<Stack
alignItems="center"
flexWrap="wrap"
direction="row"
sx={{ marginTop: '30px' }}
>
<Skeleton
variant="rectangular"
width="100%"
height={70}
sx={{ marginTop: '30px' }}
/>
<Skeleton
variant="rectangular"
width="100%"
height={30}
sx={{ marginTop: '20px' }}
/>
<Skeleton
variant="rectangular"
width="100%"
height={15}
sx={{ marginTop: '20px' }}
/>
<Skeleton
variant="rectangular"
width={250}
height={15}
sx={{ marginTop: '10px' }}
/>
<Skeleton
variant="rectangular"
width="100%"
height={70}
sx={{ marginTop: '50px' }}
/>
<Skeleton
variant="rectangular"
width="100%"
height={30}
sx={{ marginTop: '20px' }}
/>
<Skeleton
variant="rectangular"
width="100%"
height={15}
sx={{ marginTop: '20px' }}
/>
<Skeleton
variant="rectangular"
width={250}
height={15}
sx={{ marginTop: '10px' }}
/>
</Stack>
);
case 'idle':
const assesments = assignments.filter(a => a.type === 'assessment');
const projects = assignments.filter(a => a.type === 'project');
return (
<Container sx={externalContainer} disableGutters>
<Fab
sx={{ width: '100%' }}
color="primary"
aria-label="add"
variant="extended"
>
<AddIcon />
Criar atividade
</Fab>
<Container sx={innerContainer} disableGutters>
<Typography sx={sectionTitle} variant="h4">
Provas
</Typography>
<Stack alignItems="center">
{assesments.length !== 0 ? (
assesments.map(a => (
<Container
key={a.id}
sx={assignmentContainer}
disableGutters
>
<Typography variant="body1" sx={assignmentTypography}>
<Link
sx={assignmentLink}
href={`/assignment/${a.id}`}
>
{a.title}
</Link>
</Typography>
<Typography
sx={assignmentDueDate}
variant="p"
component="div"
>
<strong>Data de entrega: </strong>{' '}
{capitalizeFirstLetter(
dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]')
)}
</Typography>
<Typography
sx={assignmentScores}
variant="p"
component="div"
>
<strong>Valor: </strong>
{a.scores.map(s => s.value).join(', ')} pts
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhuma prova encontrada!</p>
</Container>
)}
</Stack>
</Container>
<Container sx={innerContainer} disableGutters>
<Typography sx={sectionTitle} variant="h4">
Trabalhos
</Typography>
<Stack alignItems="center">
{projects.length !== 0 ? (
projects.map(a => (
<Container
key={a.id}
sx={assignmentContainer}
disableGutters
>
<Typography variant="body1" sx={assignmentTypography}>
<Link
sx={assignmentLink}
href={`/assignment/${a.id}`}
>
{a.title}
</Link>
</Typography>
<Typography
sx={assignmentDueDate}
variant="p"
component="div"
>
<strong>Data de entrega: </strong>{' '}
{capitalizeFirstLetter(
dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]')
)}
</Typography>
<Typography
sx={assignmentScores}
variant="p"
component="div"
>
<strong>Valor: </strong>
{a.scores.map(s => s.value).join(', ')} pts
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum trabalho encontrado!</p>
</Container>
)}
</Stack>
</Container>
</Container>
);
case 'gone':
return null;
default:
return null;
}
}
};
return layoutResolver(
assignmentsTabData && assignmentsTabData.state,
assignmentsTabData && assignmentsTabData.assignments,
layoutType
);
}
export default AssignmentsTab;

View file

@ -0,0 +1,138 @@
// ========== Desktop ==========
const desktopExternalContainer = {
display: 'flex',
flexDirection: 'column',
marginTop: '50px',
height: '100vh',
alignItems: 'flex-end',
};
const desktopInnerContainer = {
width: '90%',
marginBottom: '30px',
};
const desktopSectionTitle = {
padding: '10px',
borderBottom: '2px solid #00420D',
color: '#00420D',
};
const desktopAssignmentContainer = {
width: '95%',
padding: '20px',
borderBottom: '2px solid #BCBCBC',
};
const desktopAssignmentTypography = {};
const desktopAssignmentLink = {
color: 'black',
textDecoration: 'underline #000000',
};
const desktopAssignmentDueDate = {
marginTop: '15px',
fontSize: '15px',
};
const desktopAssignmentScores = {
fontSize: '15px',
};
const desktopEmptyStateContainer = {
display: 'flex',
justifyContent: 'center',
marginTop: '30px',
};
const desktop = {
externalContainer: desktopExternalContainer,
innerContainer: desktopInnerContainer,
sectionTitle: desktopSectionTitle,
assignmentContainer: desktopAssignmentContainer,
assignmentTypography: desktopAssignmentTypography,
assignmentLink: desktopAssignmentLink,
assignmentDueDate: desktopAssignmentDueDate,
assignmentScores: desktopAssignmentScores,
emptyStateContainer: desktopEmptyStateContainer,
};
// ========== Mobile ==========
const mobileExternalContainer = {
marginTop: '50px',
height: '100vh',
};
const mobileInnerContainer = {
width: '100%',
marginBottom: '30px',
marginTop: '30px',
};
const mobileSectionTitle = {
padding: '10px',
borderBottom: '2px solid #00420D',
color: '#00420D',
};
const mobileAssignmentContainer = {
width: '100%',
padding: '20px',
borderBottom: '2px solid #BCBCBC',
};
const mobileAssignmentTypography = {
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
};
const mobileAssignmentLink = {
color: 'black',
textDecoration: 'underline #000000',
};
const mobileAssignmentDueDate = {
marginTop: '10px',
fontSize: '12px',
};
const mobileAssignmentScores = {
fontSize: '12px',
};
const mobileEmptyStateContainer = {
display: 'flex',
justifyContent: 'center',
marginTop: '30px',
};
const mobile = {
externalContainer: mobileExternalContainer,
innerContainer: mobileInnerContainer,
sectionTitle: mobileSectionTitle,
assignmentContainer: mobileAssignmentContainer,
assignmentTypography: mobileAssignmentTypography,
assignmentLink: mobileAssignmentLink,
assignmentDueDate: mobileAssignmentDueDate,
assignmentScores: mobileAssignmentScores,
emptyStateContainer: mobileEmptyStateContainer,
};
// ========== Unset ==========
const unset = {
externalContainer: null,
innerContainer: null,
sectionTitle: null,
assignmentContainer: null,
assignmentTypography: null,
assignmentLink: null,
assignmentDueDate: null,
assignmentScores: null,
};
const styles = { desktop, mobile, unset };
export default styles;

View file

@ -0,0 +1,34 @@
function GradesTab({ gradesTabData, layoutType }) {
const layoutResolver = (state, grades, layoutType) => {
if (layoutType === 'desktop') {
switch (state) {
case 'loading':
return <h1>Loading...</h1>;
case 'idle':
return <h1>Grades Tab</h1>;
case 'gone':
return null;
default:
return null;
}
} else if (layoutType === 'mobile') {
switch (state) {
case 'loading':
return <h1>Loading...</h1>;
case 'idle':
return <h1>Grades Tab</h1>;
case 'gone':
return null;
default:
return null;
}
}
};
return layoutResolver(
gradesTabData && gradesTabData.state,
gradesTabData && gradesTabData.grades,
layoutType
);
}
export default GradesTab;

View file

@ -0,0 +1,62 @@
import {
Avatar,
AvatarGroup,
Container,
Paper,
Skeleton,
Stack,
Tab,
Tabs,
Tooltip,
Typography,
} from '@mui/material';
import { TAB_OPTIONS } from '../tabOptions';
import styles from './styles';
function Header({
layoutType,
classroom,
selectedTabOption,
onSelectTabOption,
isLoading,
}) {
const { title, paper, tabs, avatar, tooltip } = styles[layoutType];
return classroom === null ? (
<Skeleton variant="rectangular" width="100%" height={240} />
) : (
<Container disableGutters>
<Paper sx={paper(classroom.color)} elevation={4} variant="elevation">
<h1 style={title}>{classroom.name}</h1>
<Stack alignItems="center" direction="row" spacing={1}>
<AvatarGroup total={classroom.teachers.length}>
{classroom.teachers.map(t => (
<Avatar key={t.name} alt={t.name} src={t.avatar} sx={avatar} />
))}
</AvatarGroup>
<Tooltip title={classroom.teachers.map(t => t.name).join(', ')}>
<Typography sx={tooltip} variant="body3" color="text.secondary">
{classroom.teachers.map(t => t.name).join(', ')}
</Typography>
</Tooltip>
</Stack>
<Tabs
value={selectedTabOption}
onChange={onSelectTabOption}
aria-label="Tabs para informações da disciplina"
variant={layoutType === 'mobile' ? 'scrollable' : 'fullWidth'}
sx={tabs}
>
{Object.values(TAB_OPTIONS).map(option => (
<Tab
key={option.value}
label={option.lable}
disabled={isLoading && option.value !== selectedTabOption}
/>
))}
</Tabs>
</Paper>
</Container>
);
}
export default Header;

View file

@ -0,0 +1,90 @@
// ========== Desktop ==========
const desktopTitle = {
fontWeight: 500,
};
const desktopPaper = classColor => ({
width: '100%',
borderTop: `5px solid ${classColor}`,
padding: '30px',
});
const desktopTabs = {
marginLeft: '-20px',
marginRight: '-20px',
marginBottom: '-30px',
marginTop: '30px',
};
const desktopAvatar = {
width: 30,
height: 30,
};
const desktopTooltip = {
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
};
const desktop = {
title: desktopTitle,
paper: desktopPaper,
tabs: desktopTabs,
avatar: desktopAvatar,
tooltip: desktopTooltip,
};
// ========== Mobile ==========
const mobileTitle = {
fontWeight: 500,
fontSize: '25px',
};
const mobilePaper = classColor => ({
width: '100%',
borderTop: `5px solid ${classColor}`,
padding: '20px',
});
const mobileTabs = {
marginLeft: '-10px',
marginRight: '-10px',
marginBottom: '-20px',
marginTop: '30px',
};
const mobileAvatar = {
width: 30,
height: 30,
};
const mobileTooltip = {
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
};
const mobile = {
title: mobileTitle,
paper: mobilePaper,
tabs: mobileTabs,
avatar: mobileAvatar,
tooltip: mobileTooltip,
};
// ========== Unset ==========
const unset = {
title: null,
paper: null,
tabs: null,
avatar: null,
tooltip: null,
};
const styles = { desktop, mobile, unset };
export default styles;

View file

@ -1,5 +1,5 @@
import { Avatar, Container, Skeleton, Stack, Typography } from '@mui/material'; import { Avatar, Container, Skeleton, Stack, Typography } from '@mui/material';
import { createArrayFrom1ToN } from '../../../utils/createArrayFrom1ToN'; import { createArrayFrom1ToN } from '../../../../utils/createArrayFrom1ToN';
import styles from './styles'; import styles from './styles';
function PeopleTab({ layoutType, peopleTabData }) { function PeopleTab({ layoutType, peopleTabData }) {

View file

@ -0,0 +1,51 @@
import { Container } from '@mui/system';
import AnnouncementsTab from './AnnouncementsTab';
import AssignmentsTab from './AssignmentsTab';
import GradesTab from './GradesTab';
import Header from './Header';
import PeopleTab from './PeopleTab';
import styles from './styles';
function View({
layoutType,
classroom,
selectedTabOption,
onSelectTabOption,
announcementsTabData,
assignmentsTabData,
peopleTabData,
gradesTabData,
user,
onChangeEditInput,
onSaveEditChanges,
isLoading,
}) {
const { container } = styles[layoutType];
return (
<Container disableGutters sx={container}>
<Header
layoutType={layoutType}
classroom={classroom && classroom}
selectedTabOption={selectedTabOption}
onSelectTabOption={onSelectTabOption}
isLoading={isLoading}
/>
<AnnouncementsTab
layoutType={layoutType}
announcementsTabData={announcementsTabData}
classroom={classroom && classroom}
user={user && user}
onChangeEditInput={onChangeEditInput}
onSaveEditChanges={onSaveEditChanges}
/>
<AssignmentsTab
layoutType={layoutType}
assignmentsTabData={assignmentsTabData}
/>
<PeopleTab layoutType={layoutType} peopleTabData={peopleTabData} />
<GradesTab layoutType={layoutType} gradesTabData={gradesTabData} />
</Container>
);
}
export default View;

View file

@ -0,0 +1,156 @@
import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import { useUser } from '../../../context/user';
import useLayoutType from '../../../hooks/useLayoutType';
import { TAB_OPTIONS } from './tabOptions';
import View from './View';
function Classroom() {
const params = useParams();
const layoutType = useLayoutType();
const { userService, state } = useUser();
const [classroom, setClassroom] = useState(null);
const [tabData, setTabData] = useState(null);
const [selectedTabOption, setSelectedTabOption] = useState(
TAB_OPTIONS.announcements.value
);
const onChangeEditInput = e => {
const name = e.target.name;
const value = e.target.value;
setClassroom(prev => ({ ...prev, [name]: value }));
};
const onSaveEditChanges = () => {
console.log('Saving edit changes...');
console.log(classroom);
};
const fetchAndPopulateAnnouncementsTabData = useCallback(async () => {
setTabData({ tab: 'announcements', state: 'loading' });
const announcements = await userService.fetchClassroomAnnouncements(
params.id
);
const upcomingAssignments =
await userService.fetchUpcomingAssignmentsByClassId(params.id);
setTabData({
tab: 'announcements',
state: 'idle',
announcements: [...announcements.data],
upcomingAssignments: [...upcomingAssignments.data],
});
}, [userService, params.id]);
const fetchAndPopulateAssignmentsTabData = useCallback(async () => {
setTabData({ tab: 'assignments', state: 'loading' });
const assignments = await userService.fetchAssignmentsByClassId(params.id);
setTabData({
tab: 'assignments',
state: 'idle',
assignments: [...assignments.data],
});
}, [userService, params.id]);
useEffect(() => {
async function getClassroomById(classId) {
document.title = 'Carregando...';
const result = await userService.fetchClassroomById(classId);
setClassroom(result.data);
}
function updateDocumentTitle() {
if (classroom !== null) {
document.title = classroom.name;
}
}
if (!classroom) {
getClassroomById(params.id);
}
updateDocumentTitle();
}, [userService, userService.fetchClassroomById, params, classroom]);
const fetchAndPopulatePeopleTabData = useCallback(async () => {
setTabData({ tab: 'people', state: 'loading' });
const people = await userService.fetchPeopleByClassId(params.id);
setTabData({
tab: 'people',
state: 'idle',
people: [...people.data],
});
}, [userService, params.id]);
const fetchAndPopulateGradesTabData = useCallback(async () => {
setTabData({ tab: 'people', state: 'loading' });
const grades = await userService.fetchPeopleByClassId(params.id);
setTabData({
tab: 'grades',
state: 'idle',
grades: [...grades.data],
});
}, [userService, params.id]);
useEffect(() => {
async function getSelectedTabData() {
switch (selectedTabOption) {
case TAB_OPTIONS.announcements.value:
fetchAndPopulateAnnouncementsTabData();
break;
case TAB_OPTIONS.assignments.value:
fetchAndPopulateAssignmentsTabData();
break;
case TAB_OPTIONS.people.value:
fetchAndPopulatePeopleTabData();
break;
case TAB_OPTIONS.grades.value:
fetchAndPopulateGradesTabData();
break;
default:
console.log('Invalid tab option');
}
}
getSelectedTabData();
}, [
selectedTabOption,
params,
fetchAndPopulateAnnouncementsTabData,
fetchAndPopulateAssignmentsTabData,
fetchAndPopulatePeopleTabData,
fetchAndPopulateGradesTabData,
]);
return (
<View
layoutType={layoutType}
classroom={classroom}
selectedTabOption={selectedTabOption}
onSelectTabOption={(_, value) => setSelectedTabOption(value)}
announcementsTabData={
tabData && tabData.tab === 'announcements' ? tabData : { state: 'gone' }
}
assignmentsTabData={
tabData && tabData.tab === 'assignments' ? tabData : { state: 'gone' }
}
peopleTabData={
tabData && tabData.tab === 'people' ? tabData : { state: 'gone' }
}
gradesTabData={
tabData && tabData.tab === 'grades' ? tabData : { state: 'gone' }
}
user={state && state.user}
onChangeEditInput={onChangeEditInput}
onSaveEditChanges={onSaveEditChanges}
isLoading={tabData && tabData.state === 'loading'}
/>
);
}
export default Classroom;

View file

@ -0,0 +1,20 @@
const TAB_OPTIONS = {
announcements: {
value: 0,
lable: 'Comunicados',
},
assignments: {
value: 1,
lable: 'Atividades',
},
people: {
value: 2,
lable: 'Pessoas',
},
grades: {
value: 3,
lable: 'Notas',
},
};
export { TAB_OPTIONS };

View file

@ -0,0 +1,200 @@
import { Grid, Skeleton, Stack } from '@mui/material';
import { Container } from '@mui/system';
import AssignmentCard from '../../../components/AssignmentCard';
import ClassCard from '../../../components/ClassCard';
import { createArrayFrom1ToN } from '../../../utils/createArrayFrom1ToN';
import styles from './styles';
function View({
layoutType,
classrooms,
assignmentsToReview,
onClickClassCard,
}) {
const { container, divider, assignmentsStack, onClickAssignmentCard } =
styles[layoutType];
if (layoutType === 'desktop') {
return (
<Grid sx={container} container spacing={2}>
<Grid item xs={8}>
<h1>Minhas Turmas</h1>
<Stack alignItems="center" flexWrap="wrap" direction="row" gap="30px">
{classrooms === null ? (
createArrayFrom1ToN(6).map(i => (
<Skeleton
key={i}
variant="rectangular"
width={390}
height={145}
/>
))
) : classrooms.length !== 0 ? (
classrooms.map(classroom => (
<ClassCard
key={classroom.name}
abbreviation={classroom.abbreviation}
title={classroom.name}
color={classroom.color}
teachers={classroom.teachers}
course={classroom.course}
layoutType={layoutType}
onClick={() => onClickClassCard(classroom.id)}
/>
))
) : (
<Container
sx={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
}}
disableGutters
>
<p>Nenhuma sala de aula encontrada!</p>
</Container>
)}
</Stack>
</Grid>
<Grid sx={divider} item xs={4}>
<h1>Atividades para corrigir</h1>
<Stack
sx={assignmentsStack}
alignItems="end"
flexWrap="wrap"
direction="row"
gap="30px"
>
{assignmentsToReview === null ? (
createArrayFrom1ToN(6).map(i => (
<Skeleton
key={i}
variant="rectangular"
width="35em"
height={145}
/>
))
) : assignmentsToReview.length !== 0 ? (
assignmentsToReview.map(assignment => (
<AssignmentCard
key={assignment.title}
title={assignment.title}
classrooms={assignment.classrooms}
dueDate={assignment.dueDate}
scores={assignment.scores}
layoutType={layoutType}
deliveredByStudents={assignment.deliveredByStudents}
reviewed={assignment.reviewed}
isAssignmentToReview={assignment.status !== null}
total={assignment.total}
onClick={() => onClickAssignmentCard(assignment.id)}
/>
))
) : (
<Container
sx={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
}}
disableGutters
>
<p>Nenhuma atividade encontrada!</p>
</Container>
)}
</Stack>
</Grid>
</Grid>
);
} else if (layoutType === 'mobile') {
return (
<Stack sx={container}>
<h1>Minhas Turmas</h1>
<Stack
alignItems="center"
justifyContent="center"
flexWrap="wrap"
direction="row"
gap="30px"
>
{classrooms === null ? (
createArrayFrom1ToN(6).map(i => (
<Skeleton
key={i}
variant="rectangular"
width="100%"
height={245}
/>
))
) : classrooms.length !== 0 ? (
classrooms.map(classroom => (
<ClassCard
key={classroom.name}
abbreviation={classroom.abbreviation}
title={classroom.name}
color={classroom.color}
teachers={classroom.teachers}
course={classroom.course}
layoutType={layoutType}
onClick={() => onClickClassCard(classroom.id)}
/>
))
) : (
<Container disableGutters>
<p>Nenhuma sala de aula encontrada!</p>
</Container>
)}
</Stack>
<h1 style={divider}>Atividades para corrigir</h1>
<Stack
sx={assignmentsStack}
alignItems="center"
justifyContent="center"
flexWrap="wrap"
direction="row"
gap="30px"
>
{assignmentsToReview === null ? (
createArrayFrom1ToN(6).map(i => (
<Skeleton
key={i}
variant="rectangular"
width="35em"
height={145}
/>
))
) : assignmentsToReview.length !== 0 ? (
assignmentsToReview.map(assignment => (
<AssignmentCard
key={assignment.title}
title={assignment.title}
classrooms={assignment.classrooms}
dueDate={assignment.dueDate}
scores={assignment.scores}
layoutType={layoutType}
deliveredByStudents={assignment.deliveredByStudents}
reviewed={assignment.reviewed}
isAssignmentToReview={assignment.status !== null}
total={assignment.total}
onClick={() => onClickAssignmentCard(assignment.id)}
/>
))
) : (
<Container
sx={{
height: '100vh',
display: 'flex',
justifyContent: 'center',
}}
disableGutters
>
<p>Nenhuma atividade encontrada!</p>
</Container>
)}
</Stack>
</Stack>
);
}
}
export default View;

View file

@ -0,0 +1,51 @@
import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useUser } from '../../../context/user';
import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
import useLayoutType from '../../../hooks/useLayoutType';
import View from './View';
function Home() {
useDocumentTitle('Página Inicial');
const navigate = useNavigate();
const layoutType = useLayoutType();
const { userService } = useUser();
const [classrooms, setClassrooms] = useState(null);
const [assignmentsToReview, setAssignmentsToReview] = useState(null);
useEffect(() => {
async function getClassrooms() {
const result = await userService.fetchClassrooms();
setClassrooms(result.data);
}
getClassrooms();
}, [userService, userService.fetchClassrooms]);
useEffect(() => {
async function getAssignmentsToReview() {
const result = await userService.fetchAssignmentsToReview();
setAssignmentsToReview(result.data);
}
getAssignmentsToReview();
}, [userService, userService.fetchAllAssignments]);
const onClickClassCard = id => {
navigate(`/class/${id}`);
};
const onClickAssignmentCard = id => {
navigate(`/assignment/${id}`);
};
return (
<View
layoutType={layoutType}
classrooms={classrooms}
assignmentsToReview={assignmentsToReview}
onClickClassCard={onClickClassCard}
onClickAssignmentCard={onClickAssignmentCard}
/>
);
}
export default Home;

View file

@ -0,0 +1,42 @@
// ========== Desktop ==========
const desktopContainer = {
height: '100vh',
margin: 0,
};
const desktopDivider = {
borderLeft: '4px solid #CFCFCF',
};
const desktop = {
container: desktopContainer,
divider: desktopDivider,
};
// ========== Mobile ==========
const mobileContainer = {
height: 'inherit',
width: '100%',
padding: '10px 20px ',
margin: 0,
};
const mobileDivider = {
borderTop: '2px solid #CFCFCF',
paddingTop: '15px',
};
const mobile = {
container: mobileContainer,
divider: mobileDivider,
};
// ========== Unset ==========
const unset = {
container: null,
divider: null,
assignmentsStack: null,
};
const styles = { desktop, mobile, unset };
export default styles;

View file

@ -8,11 +8,11 @@ import {
Stack, Stack,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
import AnnouncementCard from '../../../components/AnnouncementCard'; import AnnouncementCard from '../../../../components/AnnouncementCard';
import { createArrayFrom1ToN } from '../../../utils/createArrayFrom1ToN'; import { createArrayFrom1ToN } from '../../../../utils/createArrayFrom1ToN';
import styles from './styles'; import styles from './styles';
import jitsiLogo from '../../../assets/jitsi.svg'; import jitsiLogo from '../../../../assets/jitsi.svg';
function AnnouncementsTab({ layoutType, announcementsTabData, classroom }) { function AnnouncementsTab({ layoutType, announcementsTabData, classroom }) {
const { container, emptyStateContainer } = styles[layoutType]; const { container, emptyStateContainer } = styles[layoutType];
@ -64,7 +64,9 @@ function AnnouncementsTab({ layoutType, announcementsTabData, classroom }) {
sx={{ display: 'flex', justifyContent: 'row' }} sx={{ display: 'flex', justifyContent: 'row' }}
> >
<img src={jitsiLogo} alt="Jitsi Meet" /> <img src={jitsiLogo} alt="Jitsi Meet" />
<h3 style={{ fontWeight: 500 }}>Jitsi</h3> <h3 style={{ fontWeight: 500 }}>
Sala de aula virtual
</h3>
</Container> </Container>
<Button <Button
@ -205,7 +207,7 @@ function AnnouncementsTab({ layoutType, announcementsTabData, classroom }) {
sx={{ display: 'flex', justifyContent: 'row' }} sx={{ display: 'flex', justifyContent: 'row' }}
> >
<img src={jitsiLogo} alt="Jitsi Meet" /> <img src={jitsiLogo} alt="Jitsi Meet" />
<h3 style={{ fontWeight: 500 }}>Jitsi</h3> <h3 style={{ fontWeight: 500 }}>Sala de aula virtual</h3>
</Container> </Container>
<Button <Button

View file

@ -1,6 +1,6 @@
import { Container, Link, Skeleton, Stack, Typography } from '@mui/material'; import { Container, Link, Skeleton, Stack, Typography } from '@mui/material';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { capitalizeFirstLetter } from '../../../utils/capitalizeFirstLetter'; import { capitalizeFirstLetter } from '../../../../utils/capitalizeFirstLetter';
import styles from './styles'; import styles from './styles';
function AssignmentsTab({ assignmentsTabData, layoutType }) { function AssignmentsTab({ assignmentsTabData, layoutType }) {

View file

@ -0,0 +1,244 @@
import { Avatar, Container, Skeleton, Stack, Typography } from '@mui/material';
import { createArrayFrom1ToN } from '../../../../utils/createArrayFrom1ToN';
import styles from './styles';
function PeopleTab({ layoutType, peopleTabData }) {
const layoutResolver = (state, people, layoutType) => {
const {
externalContainer,
sectionContainer,
sectionTitle,
personContainer,
personAvatar,
personName,
emptyStateContainer,
} = styles[layoutType];
if (layoutType === 'desktop') {
switch (state) {
case 'loading':
return (
<Container sx={externalContainer} disableGutters>
<Stack alignItems="center">
<Skeleton
variant="rectangular"
width="90%"
height={70}
sx={{ marginBottom: '30px' }}
/>
<Stack
flexDirection="row"
alignItems="center"
sx={{ width: '90%', marginLeft: '20px' }}
>
<Skeleton variant="circular" width={60} height={60} />
<Skeleton
variant="rectangular"
width="70%"
height={40}
sx={{ marginLeft: '15px' }}
/>
</Stack>
</Stack>
<Stack alignItems="center">
<Skeleton
variant="rectangular"
width="90%"
height={70}
sx={{ marginBottom: '30px', marginTop: '50px' }}
/>
{createArrayFrom1ToN(5).map(i => (
<Stack
key={i}
flexDirection="row"
alignItems="center"
sx={{ width: '90%', marginLeft: '20px', marginTop: '25px' }}
>
<Skeleton variant="circular" width={60} height={60} />
<Skeleton
variant="rectangular"
width="70%"
height={40}
sx={{ marginLeft: '15px' }}
/>
</Stack>
))}
</Stack>
</Container>
);
case 'idle':
const professors = people.filter(p => p.role === 'PROFESSOR');
const students = people.filter(p => p.role === 'STUDENT');
return (
<Container sx={externalContainer} disableGutters>
<Container sx={sectionContainer} disableGutters>
<Typography sx={sectionTitle} variant="h4">
Docentes
</Typography>
<Stack alignItems="center">
{professors.length !== 0 ? (
professors.map(p => (
<Container key={p.id} sx={personContainer} disableGutters>
<Avatar alt={p.name} src={p.avatar} sx={personAvatar} />
<Typography sx={personName} variant="h5">
{p.name}
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum professor encontrado!</p>
</Container>
)}
</Stack>
</Container>
<Container sx={sectionContainer} disableGutters>
<Typography sx={sectionTitle} variant="h4">
Discentes
</Typography>
<Stack alignItems="center">
{students.length !== 0 ? (
students.map(p => (
<Container key={p.id} sx={personContainer} disableGutters>
<Avatar alt={p.name} src={p.avatar} sx={personAvatar} />
<Typography sx={personName} variant="h5">
{p.name}
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum estudante encontrado!</p>
</Container>
)}
</Stack>
</Container>
</Container>
);
case 'gone':
return null;
default:
return null;
}
} else if (layoutType === 'mobile') {
switch (state) {
case 'loading':
return (
<Container sx={externalContainer} disableGutters>
<Stack alignItems="center">
<Skeleton
variant="rectangular"
width="90%"
height={50}
sx={{ marginBottom: '30px' }}
/>
<Stack
flexDirection="row"
alignItems="center"
sx={{ width: '90%', marginLeft: '20px' }}
>
<Skeleton variant="circular" width={40} height={40} />
<Skeleton
variant="rectangular"
width="80%"
height={30}
sx={{ marginLeft: '15px' }}
/>
</Stack>
</Stack>
<Stack alignItems="center">
<Skeleton
variant="rectangular"
width="90%"
height={50}
sx={{ marginBottom: '30px', marginTop: '50px' }}
/>
{createArrayFrom1ToN(5).map(i => (
<Stack
key={i}
flexDirection="row"
alignItems="center"
sx={{ width: '90%', marginLeft: '20px', marginTop: '25px' }}
>
<Skeleton variant="circular" width={40} height={40} />
<Skeleton
variant="rectangular"
width="80%"
height={30}
sx={{ marginLeft: '15px' }}
/>
</Stack>
))}
</Stack>
</Container>
);
case 'idle':
const professors = people.filter(p => p.role === 'PROFESSOR');
const students = people.filter(p => p.role === 'STUDENT');
return (
<Container sx={externalContainer} disableGutters>
<Container sx={sectionContainer} disableGutters>
<Typography sx={sectionTitle} variant="h5">
Docentes
</Typography>
<Stack alignItems="center">
{professors.length !== 0 ? (
professors.map(p => (
<Container key={p.id} sx={personContainer} disableGutters>
<Avatar alt={p.name} src={p.avatar} sx={personAvatar} />
<Typography sx={personName} variant="body1">
{p.name}
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum professor encontrado!</p>
</Container>
)}
</Stack>
</Container>
<Container sx={sectionContainer} disableGutters>
<Typography sx={sectionTitle} variant="h5">
Discentes
</Typography>
<Stack alignItems="center">
{students.length !== 0 ? (
students.map(p => (
<Container key={p.id} sx={personContainer} disableGutters>
<Avatar alt={p.name} src={p.avatar} sx={personAvatar} />
<Typography sx={personName} variant="body1">
{p.name}
</Typography>
</Container>
))
) : (
<Container sx={emptyStateContainer} disableGutters>
<p>Nenhum estudante encontrado!</p>
</Container>
)}
</Stack>
</Container>
</Container>
);
case 'gone':
return null;
default:
return null;
}
}
};
return layoutResolver(
peopleTabData && peopleTabData.state,
peopleTabData && peopleTabData.people,
layoutType
);
}
export default PeopleTab;

View file

@ -0,0 +1,116 @@
// ========== Desktop ==========
const desktopExternalContainer = {
marginTop: '50px',
height: '100vh',
};
const desktopSectionContainer = {
width: '90%',
marginBottom: '30px',
};
const desktopSectionTitle = {
padding: '10px',
borderBottom: '2px solid #00420D',
color: '#00420D',
};
const desktopPersonContainer = {
width: '95%',
padding: '20px',
borderBottom: '2px solid #BCBCBC',
display: 'flex',
alignItems: 'center',
};
const desktopPersonAvatar = {
width: '60px',
height: '60px',
marginRight: '15px',
};
const desktopPersonName = {};
const desktopEmptyStateContainer = {
display: 'flex',
justifyContent: 'center',
marginTop: '30px',
};
const desktop = {
externalContainer: desktopExternalContainer,
sectionContainer: desktopSectionContainer,
sectionTitle: desktopSectionTitle,
personContainer: desktopPersonContainer,
personAvatar: desktopPersonAvatar,
personName: desktopPersonName,
emptyStateContainer: desktopEmptyStateContainer,
};
// ========== Mobile ==========
const mobileExternalContainer = {
marginTop: '50px',
height: '100vh',
};
const mobileSectionContainer = {
width: '90%',
marginBottom: '30px',
};
const mobileSectionTitle = {
padding: '10px',
borderBottom: '2px solid #00420D',
color: '#00420D',
};
const mobilePersonContainer = {
width: '95%',
padding: '20px',
borderBottom: '2px solid #BCBCBC',
display: 'flex',
alignItems: 'center',
};
const mobilePersonAvatar = {
width: '40px',
height: '40px',
marginRight: '15px',
};
const mobilePersonName = {
overflow: 'hidden',
textOverflow: 'ellipsis',
display: '-webkit-box',
WebkitLineClamp: 1,
WebkitBoxOrient: 'vertical',
};
const mobileEmptyStateContainer = {
display: 'flex',
justifyContent: 'center',
marginTop: '30px',
};
const mobile = {
externalContainer: mobileExternalContainer,
sectionContainer: mobileSectionContainer,
sectionTitle: mobileSectionTitle,
personContainer: mobilePersonContainer,
personAvatar: mobilePersonAvatar,
personName: mobilePersonName,
emptyStateContainer: mobileEmptyStateContainer,
};
// ========== Unset ==========
const unset = {
externalContainer: null,
sectionContainer: null,
sectionTitle: null,
personContainer: null,
personAvatar: null,
personName: null,
};
const styles = { desktop, mobile, unset };
export default styles;

View file

@ -1,20 +1,14 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useUser } from '../../context/user'; import { useUser } from '../../../context/user';
import useLayoutType from '../../hooks/useLayoutType'; import useLayoutType from '../../../hooks/useLayoutType';
import { TAB_OPTIONS } from './tabOptions'; import { TAB_OPTIONS } from './tabOptions';
import View from './View'; import View from './View';
function Classroom() { function Classroom() {
const params = useParams(); const params = useParams();
const layoutType = useLayoutType(); const layoutType = useLayoutType();
const { const { userService } = useUser();
fetchClassroomById,
fetchClassroomAnnouncements,
fetchUpcomingAssignmentsByClassId,
fetchAssignmentsByClassId,
fetchPeopleByClassId,
} = useUser();
const [classroom, setClassroom] = useState(null); const [classroom, setClassroom] = useState(null);
const [tabData, setTabData] = useState(null); const [tabData, setTabData] = useState(null);
const [selectedTabOption, setSelectedTabOption] = useState( const [selectedTabOption, setSelectedTabOption] = useState(
@ -23,10 +17,11 @@ function Classroom() {
const fetchAndPopulateAnnouncementsTabData = useCallback(async () => { const fetchAndPopulateAnnouncementsTabData = useCallback(async () => {
setTabData({ tab: 'announcements', state: 'loading' }); setTabData({ tab: 'announcements', state: 'loading' });
const announcements = await fetchClassroomAnnouncements(params.id); const announcements = await userService.fetchClassroomAnnouncements(
const upcomingAssignments = await fetchUpcomingAssignmentsByClassId(
params.id params.id
); );
const upcomingAssignments =
await userService.fetchUpcomingAssignmentsByClassId(params.id);
setTabData({ setTabData({
tab: 'announcements', tab: 'announcements',
@ -34,33 +29,29 @@ function Classroom() {
announcements: [...announcements.data], announcements: [...announcements.data],
upcomingAssignments: [...upcomingAssignments.data], upcomingAssignments: [...upcomingAssignments.data],
}); });
}, [ }, [userService, params.id]);
fetchClassroomAnnouncements,
fetchUpcomingAssignmentsByClassId,
params.id,
]);
const fetchAndPopulateAssignmentsTabData = useCallback(async () => { const fetchAndPopulateAssignmentsTabData = useCallback(async () => {
setTabData({ tab: 'assignments', state: 'loading' }); setTabData({ tab: 'assignments', state: 'loading' });
const assignments = await fetchAssignmentsByClassId(params.id); const assignments = await userService.fetchAssignmentsByClassId(params.id);
setTabData({ setTabData({
tab: 'assignments', tab: 'assignments',
state: 'idle', state: 'idle',
assignments: [...assignments.data], assignments: [...assignments.data],
}); });
}, [fetchAssignmentsByClassId, params.id]); }, [userService, params.id]);
const fetchAndPopulatePoepleTabData = useCallback(async () => { const fetchAndPopulatePeopleTabData = useCallback(async () => {
setTabData({ tab: 'people', state: 'loading' }); setTabData({ tab: 'people', state: 'loading' });
const people = await fetchPeopleByClassId(params.id); const people = await userService.fetchPeopleByClassId(params.id);
setTabData({ setTabData({
tab: 'people', tab: 'people',
state: 'idle', state: 'idle',
people: [...people.data], people: [...people.data],
}); });
}, [fetchPeopleByClassId, params.id]); }, [userService, params.id]);
useEffect(() => { useEffect(() => {
async function getSelectedTabData() { async function getSelectedTabData() {
@ -72,7 +63,7 @@ function Classroom() {
fetchAndPopulateAssignmentsTabData(); fetchAndPopulateAssignmentsTabData();
break; break;
case TAB_OPTIONS.people.value: case TAB_OPTIONS.people.value:
fetchAndPopulatePoepleTabData(); fetchAndPopulatePeopleTabData();
break; break;
default: default:
console.log('Invalid tab option'); console.log('Invalid tab option');
@ -84,13 +75,13 @@ function Classroom() {
params, params,
fetchAndPopulateAnnouncementsTabData, fetchAndPopulateAnnouncementsTabData,
fetchAndPopulateAssignmentsTabData, fetchAndPopulateAssignmentsTabData,
fetchAndPopulatePoepleTabData, fetchAndPopulatePeopleTabData,
]); ]);
useEffect(() => { useEffect(() => {
async function getClassroomById(classId) { async function getClassroomById(classId) {
document.title = 'Carregando...'; document.title = 'Carregando...';
const result = await fetchClassroomById(classId); const result = await userService.fetchClassroomById(classId);
setClassroom(result.data); setClassroom(result.data);
} }
@ -102,7 +93,7 @@ function Classroom() {
getClassroomById(params.id); getClassroomById(params.id);
updateDocumentTitle(); updateDocumentTitle();
}, [fetchClassroomById, params, classroom]); }, [userService, userService.fetchClassroomById, params, classroom]);
return ( return (
<View <View

View file

@ -0,0 +1,34 @@
// ========== Desktop ==========
const desktopContainer = {
width: '100%',
height: '100vh',
backgroundColor: '#red',
padding: 0,
margin: 0,
marginTop: '50px',
};
const desktop = {
container: desktopContainer,
};
// ========== Mobile ==========
const mobileContainer = {
width: '90%',
backgroundColor: '#red',
padding: 0,
marginTop: '30px',
paddingBottom: '100px',
};
const mobile = {
container: mobileContainer,
};
// ========== Unset ==========
const unset = {
container: null,
};
const styles = { desktop, mobile, unset };
export default styles;

View file

@ -1,10 +1,10 @@
import { Container, Grid, Skeleton, Stack } from '@mui/material'; import { Container, Grid, Skeleton, Stack } from '@mui/material';
import ClassCard from '../../components/ClassCard'; import ClassCard from '../../../components/ClassCard';
import AssignmentCard from '../../components/AssignmentCard'; import AssignmentCard from '../../../components/AssignmentCard';
import styles from './styles'; import styles from './styles';
import { createArrayFrom1ToN } from '../../utils/createArrayFrom1ToN'; import { createArrayFrom1ToN } from '../../../utils/createArrayFrom1ToN';
function View({ function View({
layoutType, layoutType,

View file

@ -1,33 +1,33 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useUser } from '../../context/user'; import { useUser } from '../../../context/user';
import { useDocumentTitle } from '../../hooks/useDocumentTitle'; import { useDocumentTitle } from '../../../hooks/useDocumentTitle';
import useLayoutType from '../../hooks/useLayoutType'; import useLayoutType from '../../../hooks/useLayoutType';
import View from './View'; import View from './View';
function Home() { function Home() {
useDocumentTitle('Página Inicial'); useDocumentTitle('Página Inicial');
const navigate = useNavigate(); const navigate = useNavigate();
const layoutType = useLayoutType(); const layoutType = useLayoutType();
const { fetchClassrooms, fetchAllAssignments } = useUser(); const { userService } = useUser();
const [classrooms, setClassrooms] = useState(null); const [classrooms, setClassrooms] = useState(null);
const [assignments, setAssignments] = useState(null); const [assignments, setAssignments] = useState(null);
useEffect(() => { useEffect(() => {
async function getClassrooms() { async function getClassrooms() {
const result = await fetchClassrooms(); const result = await userService.fetchClassrooms();
setClassrooms(result.data); setClassrooms(result.data);
} }
getClassrooms(); getClassrooms();
}, [fetchClassrooms]); }, [userService, userService.fetchClassrooms]);
useEffect(() => { useEffect(() => {
async function getAssignments() { async function getAssignments() {
const result = await fetchAllAssignments(); const result = await userService.fetchAllAssignments();
setAssignments(result.data); setAssignments(result.data);
} }
getAssignments(); getAssignments();
}, [fetchAllAssignments]); }, [userService, userService.fetchAllAssignments]);
const onClickClassCard = id => { const onClickClassCard = id => {
navigate(`/class/${id}`); navigate(`/class/${id}`);

27
src/services/professor.js Normal file
View file

@ -0,0 +1,27 @@
import { ProfessorApi } from '../utils/mocks/api';
export default class ProfessorService {
constructor(user) {
this.user = user;
}
fetchClassrooms = () => ProfessorApi.getClassrooms(this.user.id);
fetchClassroomById = classId => ProfessorApi.getClassroomById(classId);
fetchAssignmentsByClassId = classId =>
ProfessorApi.getAssignmentsByClassId(classId);
fetchAssignmentsToReview = () =>
ProfessorApi.getAssignmentsToReview(this.user.id);
fetchClassroomAnnouncements = classId =>
ProfessorApi.getClassroomAnnouncementsById(classId);
fetchPeopleByClassId = classId => ProfessorApi.getPeopleByClassId(classId);
fetchGradesByClassId = classId => ProfessorApi.getGradesByClassId(classId);
fetchUpcomingAssignmentsByClassId = classId =>
ProfessorApi.getUpcomingAssignmentsByClassId(classId);
}

31
src/services/provider.js Normal file
View file

@ -0,0 +1,31 @@
export const UserServiceProvider = (function () {
let instance;
async function createInstance(user) {
switch (user.role) {
case 'STUDENT':
const studentService = await import('./student');
if (studentService) {
return new studentService.default(user);
}
break;
case 'PROFESSOR':
const professorService = await import('./professor');
if (professorService) {
return new professorService.default(user);
}
break;
default:
throw new Error('Invalid Role!');
}
}
return {
getInstance: async function (user) {
if (!instance) {
instance = await createInstance(user);
}
return instance;
},
};
})();

29
src/services/student.js Normal file
View file

@ -0,0 +1,29 @@
import { StudentApi } from '../utils/mocks/api';
export default class StudentService {
constructor(user) {
this.user = user;
}
fetchClassrooms = () => StudentApi.getClassrooms(this.user.id);
fetchAllAssignments = () => StudentApi.getAllAssignments(this.user.id);
fetchAssignmentById = assignmentId =>
StudentApi.getAssignmentById(assignmentId);
fetchAssignmentsByClassId = classId =>
StudentApi.getAssignmentsByClassId(classId);
fetchClassroomById = classId => StudentApi.getClassroomById(classId);
fetchFAQ = () => StudentApi.getFaq();
fetchClassroomAnnouncements = classId =>
StudentApi.getClassroomAnnouncementsById(classId);
fetchUpcomingAssignmentsByClassId = classId =>
StudentApi.getUpcomingAssignmentsByClassId(classId);
fetchPeopleByClassId = classId => StudentApi.getPeopleByClassId(classId);
}

View file

@ -1,120 +0,0 @@
import { sleep } from '../utils/sleep';
import {
allClassrooms,
allAssignments,
faq,
user,
authFailure,
allClassroomAnnouncements,
allUpcomingAssignments,
allPeople,
} from './mocks';
const getClassrooms = userId =>
sleep(300).then(() => {
console.log('Get classrooms ' + userId);
return {
data: allClassrooms,
};
});
const getClassroomById = classId =>
sleep(300).then(() => {
console.log('Get classroom by id ' + classId);
return {
data: allClassrooms.filter(c => c.id === classId)[0],
};
});
const getClassroomAnnouncementsById = classId =>
sleep(300).then(() => {
console.log('Get classroon announcements by id ' + classId);
return {
data: allClassroomAnnouncements.filter(c => c.classroom.id === classId),
};
});
const getUpcomingAssignmentsByClassId = classId =>
sleep(300).then(() => {
console.log('Getting upcoming assignments by class id ' + classId);
return {
data: allUpcomingAssignments.filter(
a => a.classrooms.filter(c => c.id === classId)[0]
),
};
});
const getAllAssignments = userId =>
sleep(400).then(() => {
console.log('Getting all assignments ' + userId);
return {
data: allAssignments,
};
});
const getAssignmentById = assignmentId =>
sleep(400).then(() => {
console.log('Getting assignment by id ' + assignmentId);
return {
data: allAssignments.filter(a => a.id === assignmentId)[0],
};
});
const getAssignmentsByClassId = classId =>
sleep(300).then(() => {
console.log('Getting assignments by class id ' + classId);
return {
data: allAssignments.filter(a => a.classrooms[0].id === classId),
};
});
const getPeopleByClassId = classId =>
sleep(400).then(() => {
console.log('Getting people by class id ' + classId);
return {
data: allPeople.filter(p => p.classes[0].id === classId),
};
});
const getFaq = () =>
sleep(300).then(() => {
console.log('Fetching FAQ...');
return {
data: faq,
};
});
const getUser = shouldFail =>
sleep(300).then(() => {
if (shouldFail) {
return authFailure;
} else {
window.localStorage.setItem('$USER', JSON.stringify(user));
return user;
}
});
const registerUser = (data, shouldFail) =>
sleep(300).then(() => {
if (shouldFail) {
return authFailure;
} else {
console.log(data);
window.localStorage.setItem('$USER', JSON.stringify(data));
return data;
}
});
export {
getClassrooms,
getClassroomById,
getAllAssignments,
getAssignmentById,
getAssignmentsByClassId,
getClassroomAnnouncementsById,
getUpcomingAssignmentsByClassId,
getPeopleByClassId,
getFaq,
getUser,
registerUser,
};

147
src/utils/mocks/api.js Normal file
View file

@ -0,0 +1,147 @@
import { sleep } from '../sleep';
import {
allClassrooms,
allAssignments,
faq,
studentUser,
professorUser,
authFailure,
allClassroomAnnouncements,
allUpcomingAssignments,
allPeople,
professorClassrooms,
assignmentsToReview,
grades,
} from './responses';
const CommonApi = {
getUser: (email, password) =>
sleep(300).then(() => {
let user;
if (email === 'p@test.com' && password === 'p123') {
user = professorUser;
} else if (email === 's@test.com' && password === 's123') {
user = studentUser;
} else {
return authFailure;
}
window.localStorage.setItem('$USER', JSON.stringify(user));
return user;
}),
registerUser: data =>
sleep(300).then(() => {
let userData;
if (data.email === 'p@test.com') {
userData = { ...data, role: 'PROFESSOR' };
} else if (data.email === 's@test.com') {
userData = { ...data, role: 'STUDENT' };
} else {
return authFailure;
}
window.localStorage.setItem('$USER', JSON.stringify(data));
return userData;
}),
getClassroomAnnouncementsById: classId =>
sleep(300).then(() => {
console.log('Get classroon announcements by id ' + classId);
return {
data: allClassroomAnnouncements.filter(c => c.classroom.id === classId),
};
}),
getClassroomById: classId =>
sleep(300).then(() => {
console.log('Get classroom by id ' + classId);
return {
data: allClassrooms.filter(c => c.id === classId)[0],
};
}),
getAssignmentsByClassId: classId =>
sleep(300).then(() => {
console.log('Getting assignments by class id ' + classId);
return {
data: allAssignments.filter(a => a.classrooms[0].id === classId),
};
}),
getPeopleByClassId: classId =>
sleep(400).then(() => {
console.log('Getting people by class id ' + classId);
return {
data: allPeople.filter(p => p.classes[0].id === classId),
};
}),
getUpcomingAssignmentsByClassId: classId =>
sleep(300).then(() => {
console.log('Getting upcoming assignments by class id ' + classId);
return {
data: allUpcomingAssignments.filter(
a => a.classrooms.filter(c => c.id === classId)[0]
),
};
}),
};
const StudentApi = {
...CommonApi,
getClassrooms: userId =>
sleep(300).then(() => {
console.log('Get classrooms ' + userId);
return {
data: allClassrooms,
};
}),
getAllAssignments: userId =>
sleep(400).then(() => {
console.log('Getting all assignments ' + userId);
return {
data: allAssignments,
};
}),
getAssignmentById: assignmentId =>
sleep(400).then(() => {
console.log('Getting assignment by id ' + assignmentId);
return {
data: allAssignments.filter(a => a.id === assignmentId)[0],
};
}),
getFaq: () =>
sleep(300).then(() => {
console.log('Fetching FAQ...');
return {
data: faq,
};
}),
};
const ProfessorApi = {
...CommonApi,
getClassrooms: userId =>
sleep(300).then(() => {
console.log('Get classrooms ' + userId);
return {
data: professorClassrooms,
};
}),
getAssignmentsToReview: userId =>
sleep(400).then(() => {
console.log('Getting assignments to review' + userId);
return {
data: assignmentsToReview,
};
}),
getGradesByClassId: classId =>
sleep(400).then(() => {
console.log('Getting grades' + classId);
return {
data: grades,
};
}),
};
export { StudentApi, ProfessorApi, CommonApi };

View file

@ -4,6 +4,7 @@ const allClassrooms = [
name: 'Introdução à Ciência de Dados', name: 'Introdução à Ciência de Dados',
abbreviation: 'ICD', abbreviation: 'ICD',
color: '#006FF2', color: '#006FF2',
virtualRoom: 'https://meet.jit.si/ifmg-icd-321',
teachers: [ teachers: [
{ {
id: '2342', id: '2342',
@ -22,6 +23,7 @@ const allClassrooms = [
name: 'Gestão de Projetos', name: 'Gestão de Projetos',
abbreviation: 'GP', abbreviation: 'GP',
color: '#7900F2', color: '#7900F2',
virtualRoom: 'https://meet.jit.si/ifmg-gp-123',
teachers: [ teachers: [
{ {
id: '1234', id: '1234',
@ -45,6 +47,7 @@ const allClassrooms = [
name: 'Banco de Dados II', name: 'Banco de Dados II',
abbreviation: 'BDII', abbreviation: 'BDII',
color: '#FF7A00', color: '#FF7A00',
virtualRoom: 'https://meet.jit.si/ifmg-bdii-666',
teachers: [ teachers: [
{ {
id: '6781', id: '6781',
@ -63,6 +66,7 @@ const allClassrooms = [
name: 'Contabilidade Básica', name: 'Contabilidade Básica',
abbreviation: 'CB', abbreviation: 'CB',
color: '#BB0000', color: '#BB0000',
virtualRoom: 'https://meet.jit.si/ifmg-cb-765',
teachers: [ teachers: [
{ {
id: '4321', id: '4321',
@ -80,6 +84,7 @@ const allClassrooms = [
name: 'Linguagens de Programação', name: 'Linguagens de Programação',
abbreviation: 'LP', abbreviation: 'LP',
color: '#039200', color: '#039200',
virtualRoom: 'https://meet.jit.si/ifmg-lp-333',
teachers: [ teachers: [
{ {
id: '9999', id: '9999',
@ -95,6 +100,44 @@ const allClassrooms = [
}, },
]; ];
const professorClassrooms = [
{
id: '321',
name: 'Introdução à Ciência de Dados',
abbreviation: 'ICD',
color: '#006FF2',
virtualRoom: 'https://meet.jit.si/ifmg-icd-321',
course: 'BSI 2020',
appointmentSlots: [
{ weekDay: 'Quarta-feira', start: '10:00', end: '11:40' },
{ weekDay: 'Sexta-feira', start: '10:00', end: '11:40' },
],
},
{
id: '123',
name: 'Teoria dos Grafos',
abbreviation: 'TDG',
color: '#d30000',
virtualRoom: 'https://meet.jit.si/ifmg-tdg-123',
course: 'BSI 2018',
appointmentSlots: [
{ weekDay: 'Quarta-feira', start: '11:00', end: '12:00' },
{ weekDay: 'Segunda-feira', start: '10:00', end: '11:40' },
],
},
{
id: '666',
name: 'Matemática Discreta',
abbreviation: 'MD',
color: '#149b00',
virtualRoom: 'https://meet.jit.si/ifmg-md-666',
course: 'BSI 2020',
appointmentSlots: [
{ weekDay: 'Quarta-feira', start: '9:00', end: '10:00' },
],
},
];
const allAssignments = [ const allAssignments = [
{ {
id: '5435', id: '5435',
@ -309,6 +352,80 @@ const allClassroomAnnouncements = [
}, },
]; ];
const assignmentsToReview = [
{
id: '0123',
type: 'assessment',
title:
'Prova 1 - Armazenamento de Dados. Python em CD. Armazenamento Analítico',
dueDate: '2022-07-01 23:59',
scores: [
{
classroomId: '321',
value: 30,
},
],
classrooms: professorClassrooms.filter(c => c.id === '321'),
status: 'OPEN',
deliveredByStudents: 10,
reviewed: 6,
total: 30,
},
{
id: '0128',
type: 'assessment',
title:
'Prova 2 - Visualização de Dados. Matemática e Estatística em CD. Análise de Dados',
dueDate: '2022-09-01 23:59',
scores: [
{
classroomId: '321',
value: 30,
},
],
classrooms: professorClassrooms.filter(c => c.id === '321'),
status: 'CLOSED',
deliveredByStudents: 30,
reviewed: 1,
total: 30,
},
{
id: '0129',
type: 'assessment',
title: 'Lista de Exercícios 1 - Caminhos e circuitos',
dueDate: '2022-09-01 23:59',
scores: [
{
classroomId: '123',
value: 30,
},
],
classrooms: professorClassrooms.filter(c => c.id === '123'),
status: 'CLOSED',
deliveredByStudents: 30,
reviewed: 0,
total: 30,
},
{
id: '0130',
type: 'assessment',
title: 'Lista de Exercícios 2 - Tabela verdade',
dueDate: '2022-09-01 23:59',
scores: [
{
classroomId: '666',
value: 30,
},
],
classrooms: professorClassrooms.filter(c => c.id === '666'),
status: 'OPEN',
deliveredByStudents: 0,
reviewed: 0,
total: 30,
},
];
const allUpcomingAssignments = [ const allUpcomingAssignments = [
{ {
id: '5435', id: '5435',
@ -534,7 +651,14 @@ const allPeople = [
}, },
]; ];
const user = { // TODO: Mock correct data
const grades = [
{
id: 'Some grade',
},
];
const studentUser = {
id: '123', id: '123',
ra: '0021123', ra: '0021123',
username: 'ronaldosilva', username: 'ronaldosilva',
@ -548,6 +672,25 @@ const user = {
course: 0, course: 0,
termsAgreed: true, termsAgreed: true,
year: 2018, year: 2018,
role: 'STUDENT',
};
const professorUser = {
id: '321',
ra: '0021123',
username: 'cazalbe',
email: 'carlos.junior@ifmg.edu.br',
password: '#carlos1234', // TODO: Remove this!
firstName: 'Carlos',
lastName: 'Alexandre Silva',
token: 'xkhfb9458hnsdfsi9q8345bsdf9b834yr',
phone: '31111111111',
avatar:
'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=50&q=80',
course: 0,
termsAgreed: true,
year: 2018,
role: 'PROFESSOR',
}; };
const authFailure = { const authFailure = {
@ -560,7 +703,11 @@ export {
allClassroomAnnouncements, allClassroomAnnouncements,
allPeople, allPeople,
faq, faq,
user, studentUser,
professorUser,
authFailure, authFailure,
allUpcomingAssignments, allUpcomingAssignments,
professorClassrooms,
assignmentsToReview,
grades,
}; };