diff --git a/src/app/AuthenticatedApp.js b/src/app/AuthenticatedApp.js index 80d66be..b4a9412 100644 --- a/src/app/AuthenticatedApp.js +++ b/src/app/AuthenticatedApp.js @@ -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 { useUser } from '../context/user'; import { useAuthState } from '../context/auth'; +import { useUser } from '../context/user'; 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 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 styles from './styles'; +const StudentRoutes = lazy(() => import('./StudentRoutes')); +const ProfessorRoutes = lazy(() => import('./ProfessorRoutes')); + function AuthenticatedApp() { const navigate = useNavigate(); const { state } = useUser(); @@ -24,6 +22,17 @@ function AuthenticatedApp() { const layoutType = useLayoutType(); const { container, toolbar } = styles[layoutType]; + const routeResolver = role => { + switch (role) { + case 'STUDENT': + return ; + case 'PROFESSOR': + return ; + default: + return null; + } + }; + return ( state && state.user && ( @@ -43,21 +52,7 @@ function AuthenticatedApp() { options={menuOptions(state.pathname)} layoutType={layoutType} /> - - } /> - } /> - } /> - } /> - - } /> - - - } /> - - } /> - } /> - } /> - + {routeResolver(state.user.role)} ) diff --git a/src/app/ProfessorRoutes.js b/src/app/ProfessorRoutes.js new file mode 100644 index 0000000..ced0d15 --- /dev/null +++ b/src/app/ProfessorRoutes.js @@ -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 ( + + Calendar} /> + Profile} /> + + } /> + + Information} /> + } /> + } /> + } /> + } /> + + ); +} + +export default ProfessorRoutes; diff --git a/src/app/StudentRoutes.js b/src/app/StudentRoutes.js new file mode 100644 index 0000000..1dc6b4a --- /dev/null +++ b/src/app/StudentRoutes.js @@ -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 ( + + } /> + } /> + } /> + } /> + + } /> + + + } /> + + } /> + } /> + } /> + + ); +} + +export default StudentRoutes; diff --git a/src/app/data.js b/src/app/data.js index e7048f5..647eb01 100644 --- a/src/app/data.js +++ b/src/app/data.js @@ -28,6 +28,7 @@ const menuOptions = activePath => [ isActive: activePath === '/home' || activePath === '/login' || + activePath === '/register' || activePath === '/profile' || activePath === '/' || activePath.indexOf('class') !== -1 || diff --git a/src/components/AssignmentCard/index.js b/src/components/AssignmentCard/index.js index e893b3d..fc5758b 100644 --- a/src/components/AssignmentCard/index.js +++ b/src/components/AssignmentCard/index.js @@ -18,6 +18,10 @@ function AssignmentCard({ classrooms, dueDate, scores, + deliveredByStudents, + reviewed, + total, + isAssignmentToReview, layoutType, onClick, }) { @@ -61,16 +65,30 @@ function AssignmentCard({ {classrooms.map(c => c.name).join(', ')} + Data de entrega: {' '} {capitalizeFirstLetter( dayjs(dueDate).format('dddd, DD/MM | HH:mm[h]') )} - - Valor: - {scores.map(s => s.value).join(', ')} pts - + {deliveredByStudents >= 0 && total && ( + + Entregues: {' '} + {`${deliveredByStudents} de ${total}`} + + )} + {reviewed >= 0 && total && ( + + Corrigidas: {`${reviewed} de ${total}`} + + )} + {!isAssignmentToReview && ( + + Valor: + {scores.map(s => s.value).join(', ')} pts + + )} @@ -110,10 +128,23 @@ function AssignmentCard({ dayjs(dueDate).format('dddd, DD/MM | HH:mm[h]') )} - - Valor: - {scores.map(s => s.value).join(', ')} pts - + {deliveredByStudents >= 0 && total && ( + + Entregues: {' '} + {`${deliveredByStudents} de ${total}`} + + )} + {reviewed >= 0 && total && ( + + Corrigidas: {`${reviewed} de ${total}`} + + )} + {!isAssignmentToReview && ( + + Valor: + {scores.map(s => s.value).join(', ')} pts + + )} diff --git a/src/components/ClassCard/index.js b/src/components/ClassCard/index.js index 79bf495..6c78254 100644 --- a/src/components/ClassCard/index.js +++ b/src/components/ClassCard/index.js @@ -17,6 +17,7 @@ function ClassCard({ title, color, teachers, + course, layoutType, onClick, }) { @@ -45,23 +46,30 @@ function ClassCard({ > {title} - - - {teachers.map(t => ( - - ))} - - t.name).join(', ')}> - - {teachers.map(t => t.name).join(', ')} - - - + {teachers && ( + + + {teachers.map(t => ( + + ))} + + t.name).join(', ')}> + + {teachers.map(t => t.name).join(', ')} + + + + )} + {course && {course}} @@ -82,21 +90,24 @@ function ClassCard({ > {title} - - - {teachers.map(t => ( - - ))} - - - {teachers.map(t => t.name).join(', ')} - - + {teachers && ( + + + {teachers.map(t => ( + + ))} + + + {teachers.map(t => t.name).join(', ')} + + + )} + {course && {course}} diff --git a/src/components/FormDialog/index.js b/src/components/FormDialog/index.js new file mode 100644 index 0000000..0b8ca30 --- /dev/null +++ b/src/components/FormDialog/index.js @@ -0,0 +1,33 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, +} from '@mui/material'; + +function FormDialog({ + isOpened, + title, + contentText, + inputs, + onDismiss, + onSave, +}) { + return ( + + {title} + + {contentText} + {[...inputs]} + + + + + + + ); +} + +export default FormDialog; diff --git a/src/components/PublishAnnouncementCard/index.js b/src/components/PublishAnnouncementCard/index.js new file mode 100644 index 0000000..67501b2 --- /dev/null +++ b/src/components/PublishAnnouncementCard/index.js @@ -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 ( + + {isComposing ? ( + + + + + + + + + + ) : ( + setIsComposing(true)} + > + + + Escreva um comunicado para sua turma + + + )} + + ); +} + +export default PublishAnnouncementCard; diff --git a/src/components/PublishAnnouncementCard/styles.js b/src/components/PublishAnnouncementCard/styles.js new file mode 100644 index 0000000..4bc58fd --- /dev/null +++ b/src/components/PublishAnnouncementCard/styles.js @@ -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; diff --git a/src/context/auth.js b/src/context/auth.js index 655b8d1..59ccd82 100644 --- a/src/context/auth.js +++ b/src/context/auth.js @@ -1,5 +1,5 @@ import { createContext, useContext, useEffect, useState } from 'react'; -import { getUser, registerUser } from '../services/user-service'; +import { CommonApi } from '../utils/mocks/api'; const AuthContext = createContext(); @@ -22,10 +22,9 @@ function AuthProvider(props) { const register = data => { setState({ ...state, status: 'pending' }); - let shouldFail = false; - return registerUser(data, shouldFail).then(data => { - if (shouldFail) { + return CommonApi.registerUser(data).then(data => { + if (data.message) { return setState({ status: 'error', user: null, error: data }); } else { return setState({ status: 'success', user: data, error: null }); @@ -35,10 +34,9 @@ function AuthProvider(props) { const login = (email, password) => { setState({ ...state, status: 'pending' }); - let shouldFail = email !== 'teste@teste.com' || password !== '#teste1234'; - return getUser(shouldFail).then(data => { - if (shouldFail) { + return CommonApi.getUser(email, password).then(data => { + if (data.message) { return setState({ status: 'error', user: null, error: data }); } else { return setState({ status: 'success', user: data, error: null }); diff --git a/src/context/user.js b/src/context/user.js index 23a3df4..4225201 100644 --- a/src/context/user.js +++ b/src/context/user.js @@ -1,23 +1,14 @@ import { createContext, useContext, useEffect, useState } from 'react'; import { useLocation } from 'react-router-dom'; +import { UserServiceProvider } from '../services/provider'; import { useAuthState } from './auth'; -import { - getAllAssignments, - getAssignmentById, - getAssignmentsByClassId, - getClassroomAnnouncementsById, - getClassroomById, - getClassrooms, - getFaq, - getPeopleByClassId, - getUpcomingAssignmentsByClassId, -} from '../services/user-service'; const UserContext = createContext(); function UserProvider(props) { const { user } = useAuthState(); const { pathname } = useLocation(); + const [userService, setUserService] = useState(null); const [state, setState] = useState({ user: null, error: null, @@ -26,73 +17,23 @@ function UserProvider(props) { useEffect(() => { setState({ user, pathname }); + + async function initUserService() { + if (user) { + const instance = await UserServiceProvider.getInstance(user); + setUserService(instance); + } + } + initUserService(); }, [user, pathname]); - const fetchClassrooms = () => getClassrooms(user.id); - - 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 ( - - ); + return ; } function useUser() { - const { - state, - fetchClassrooms, - fetchAssignmentById, - fetchAllAssignments, - fetchAssignmentsByClassId, - fetchClassroomById, - fetchFAQ, - fetchClassroomAnnouncements, - fetchUpcomingAssignmentsByClassId, - fetchPeopleByClassId, - } = useContext(UserContext); + const { state, userService } = useContext(UserContext); - return { - state, - fetchClassrooms, - fetchAllAssignments, - fetchAssignmentById, - fetchAssignmentsByClassId, - fetchClassroomById, - fetchFAQ, - fetchClassroomAnnouncements, - fetchUpcomingAssignmentsByClassId, - fetchPeopleByClassId, - }; + return { state, userService }; } export { UserProvider, useUser }; diff --git a/src/screens/Assignment/index.js b/src/screens/Assignment/index.js index 3559d3b..3357c2b 100644 --- a/src/screens/Assignment/index.js +++ b/src/screens/Assignment/index.js @@ -8,14 +8,14 @@ import View from './View'; function Assignment() { const params = useParams(); const layoutType = useLayoutType(); - const { fetchAssignmentById } = useUser(); + const { userService } = useUser(); const [assignment, setAssignment] = useState(null); const dropzone = useDropzone({ maxFiles: 5 }); useEffect(() => { async function getAssignmentById(assignmentId) { document.title = 'Carregando...'; - const result = await fetchAssignmentById(assignmentId); + const result = await userService.fetchAssignmentById(assignmentId); setAssignment(result.data); } @@ -27,7 +27,7 @@ function Assignment() { getAssignmentById(params.id); updateDocumentTitle(); - }, [params, fetchAssignmentById, assignment]); + }, [params, userService, userService.fetchAssignmentById, assignment]); return ( diff --git a/src/screens/Information/index.js b/src/screens/Information/index.js index b1f7965..1042060 100644 --- a/src/screens/Information/index.js +++ b/src/screens/Information/index.js @@ -9,16 +9,16 @@ import { sectors } from './data'; function Information() { useDocumentTitle('Informações'); const layoutType = useLayoutType(); - const { fetchFAQ } = useUser(); + const { userService } = useUser(); const [faq, setFaq] = useState(null); useEffect(() => { async function getClassrooms() { - const result = await fetchFAQ(); + const result = await userService.fetchFAQ(); setFaq(result.data); } getClassrooms(); - }, [fetchFAQ]); + }, [userService, userService.fetchFAQ]); return ; } diff --git a/src/screens/professor/Classroom/AnnouncementsTab/index.js b/src/screens/professor/Classroom/AnnouncementsTab/index.js new file mode 100644 index 0000000..74fbe3b --- /dev/null +++ b/src/screens/professor/Classroom/AnnouncementsTab/index.js @@ -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 ( + + + {createArrayFrom1ToN(3).map(i => ( + + ))} + + + {createArrayFrom1ToN(4).map(i => ( + + ))} + + + ); + + case 'idle': + return ( + + + + + + + + Jitsi Meet +

+ Sala de aula virtual +

+
+ + + + setAnchorEl({ + ...anchorEl, + virtualRoom: e.currentTarget, + }) + } + aria-label="edit" + size="medium" + > + + + + + setAnchorEl({ ...anchorEl, virtualRoom: null }) + } + > + setDialogOpened('virtualRoom')} + > + Editar + + + , + ]} + onDismiss={() => onDismissEdit('virtualRoom')} + onSave={() => onSaveEdit('virtualRoom')} + /> +
+ + +
+
+ + +

Próximas Atividades

+ {announcementsTabData.upcomingAssignments.length !== 0 ? ( + announcementsTabData.upcomingAssignments.map(ua => ( + + {ua.title} + + )) + ) : ( + +

Nenhuma atividade encontrada!

+
+ )} +
+
+ + + +

+ Horários de Atendimento +

+ + + setAnchorEl({ + ...anchorEl, + appointmentSlots: e.currentTarget, + }) + } + aria-label="edit" + size="medium" + > + + + + + setAnchorEl({ ...anchorEl, appointmentSlots: null }) + } + > + setDialogOpened('appointmentSlots')} + > + Editar + + + ( + + )), + ]} + onDismiss={() => onDismissEdit('appointmentSlots')} + onSave={() => onSaveEdit('appointmentSlots')} + /> +
+ {classroom.appointmentSlots.map((appts, index) => ( + + {appts.weekDay}, {appts.start}h - {appts.end}h + + ))} +
+
+
+
+ + + setComposingTextValue(e.target.value)} + /> + {announcementsTabData.announcements.length !== 0 ? ( + announcementsTabData.announcements.map(announcement => ( + + )) + ) : ( + +

Nenhum comunicado encontrado!

+
+ )} +
+
+
+ ); + + case 'gone': + return null; + + default: + return null; + } + } else if (layoutType === 'mobile') { + switch (state) { + case 'loading': + return ( + + {createArrayFrom1ToN(3).map(i => ( + + ))} + {createArrayFrom1ToN(4).map(i => ( + + ))} + + ); + + case 'idle': + return ( + + + + + + + Jitsi Meet +

+ Sala de aula virtual +

+
+ + setAnchorEl(e.currentTarget)} + aria-label="edit" + size="medium" + > + + + + setAnchorEl(null)} + > + setDialogOpened('virtualRoom')} + > + Editar + + + , + ]} + onDismiss={() => onDismissEdit('virtualRoom')} + onSave={() => onSaveEdit('virtualRoom')} + /> +
+ +
+
+ + +

Próximas Atividades

+ + {announcementsTabData.upcomingAssignments.length !== 0 ? ( + announcementsTabData.upcomingAssignments.map(ua => ( + + {ua.title} + + )) + ) : ( + +

Nenhuma atividade encontrada!

+
+ )} +
+
+ + +

Horários de Atendimento

+ {classroom.appointmentSlots.map((appts, index) => ( + + {appts.weekDay}, {appts.start}h - {appts.end}h + + ))} +
+
+
+ + setComposingTextValue(e.target.value)} + /> + {announcementsTabData.announcements.length !== 0 ? ( + announcementsTabData.announcements.map(announcement => ( + + )) + ) : ( + +

Nenhum comunicado encontrado!

+
+ )} +
+
+ ); + + case 'gone': + return null; + + default: + return null; + } + } + }; + + return layoutResolver( + announcementsTabData && announcementsTabData.state, + layoutType + ); +} + +export default AnnouncementsTab; diff --git a/src/screens/professor/Classroom/AnnouncementsTab/styles.js b/src/screens/professor/Classroom/AnnouncementsTab/styles.js new file mode 100644 index 0000000..46b3813 --- /dev/null +++ b/src/screens/professor/Classroom/AnnouncementsTab/styles.js @@ -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; diff --git a/src/screens/professor/Classroom/AssignmentsTab/index.js b/src/screens/professor/Classroom/AssignmentsTab/index.js new file mode 100644 index 0000000..2060c40 --- /dev/null +++ b/src/screens/professor/Classroom/AssignmentsTab/index.js @@ -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 ( + + + + + + + + + + + + + + + + + + + + + + + + + ); + case 'idle': + const assesments = assignments.filter(a => a.type === 'assessment'); + const projects = assignments.filter(a => a.type === 'project'); + + return ( + + + + Criar atividade + + + + Provas + + + {assesments.length !== 0 ? ( + assesments.map(a => ( + + + + {a.title} + + + + Data de entrega: {' '} + {capitalizeFirstLetter( + dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]') + )} + + + Valor: + {a.scores.map(s => s.value).join(', ')} pts + + + )) + ) : ( + +

Nenhuma prova encontrada!

+
+ )} +
+
+ + + + Trabalhos + + + {projects.length !== 0 ? ( + projects.map(a => ( + + + + {a.title} + + + + Data de entrega: {' '} + {capitalizeFirstLetter( + dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]') + )} + + + Valor: + {a.scores.map(s => s.value).join(', ')} pts + + + )) + ) : ( + +

Nenhum trabalho encontrado!

+
+ )} +
+
+
+ ); + case 'gone': + return null; + default: + return null; + } + } else if (layoutType === 'mobile') { + switch (state) { + case 'loading': + return ( + + + + + + + + + + + + ); + case 'idle': + const assesments = assignments.filter(a => a.type === 'assessment'); + const projects = assignments.filter(a => a.type === 'project'); + + return ( + + + + Criar atividade + + + + Provas + + + {assesments.length !== 0 ? ( + assesments.map(a => ( + + + + {a.title} + + + + Data de entrega: {' '} + {capitalizeFirstLetter( + dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]') + )} + + + Valor: + {a.scores.map(s => s.value).join(', ')} pts + + + )) + ) : ( + +

Nenhuma prova encontrada!

+
+ )} +
+
+ + + + Trabalhos + + + {projects.length !== 0 ? ( + projects.map(a => ( + + + + {a.title} + + + + Data de entrega: {' '} + {capitalizeFirstLetter( + dayjs(a.dueDate).format('dddd, DD/MM | HH:mm[h]') + )} + + + Valor: + {a.scores.map(s => s.value).join(', ')} pts + + + )) + ) : ( + +

Nenhum trabalho encontrado!

+
+ )} +
+
+
+ ); + case 'gone': + return null; + default: + return null; + } + } + }; + return layoutResolver( + assignmentsTabData && assignmentsTabData.state, + assignmentsTabData && assignmentsTabData.assignments, + layoutType + ); +} + +export default AssignmentsTab; diff --git a/src/screens/professor/Classroom/AssignmentsTab/styles.js b/src/screens/professor/Classroom/AssignmentsTab/styles.js new file mode 100644 index 0000000..0493789 --- /dev/null +++ b/src/screens/professor/Classroom/AssignmentsTab/styles.js @@ -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; diff --git a/src/screens/professor/Classroom/GradesTab/index.js b/src/screens/professor/Classroom/GradesTab/index.js new file mode 100644 index 0000000..785bd50 --- /dev/null +++ b/src/screens/professor/Classroom/GradesTab/index.js @@ -0,0 +1,34 @@ +function GradesTab({ gradesTabData, layoutType }) { + const layoutResolver = (state, grades, layoutType) => { + if (layoutType === 'desktop') { + switch (state) { + case 'loading': + return

Loading...

; + case 'idle': + return

Grades Tab

; + case 'gone': + return null; + default: + return null; + } + } else if (layoutType === 'mobile') { + switch (state) { + case 'loading': + return

Loading...

; + case 'idle': + return

Grades Tab

; + case 'gone': + return null; + default: + return null; + } + } + }; + return layoutResolver( + gradesTabData && gradesTabData.state, + gradesTabData && gradesTabData.grades, + layoutType + ); +} + +export default GradesTab; diff --git a/src/screens/professor/Classroom/GradesTab/styles.js b/src/screens/professor/Classroom/GradesTab/styles.js new file mode 100644 index 0000000..e69de29 diff --git a/src/screens/professor/Classroom/Header/index.js b/src/screens/professor/Classroom/Header/index.js new file mode 100644 index 0000000..e077313 --- /dev/null +++ b/src/screens/professor/Classroom/Header/index.js @@ -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 ? ( + + ) : ( + + +

{classroom.name}

+ + + {classroom.teachers.map(t => ( + + ))} + + t.name).join(', ')}> + + {classroom.teachers.map(t => t.name).join(', ')} + + + + + {Object.values(TAB_OPTIONS).map(option => ( + + ))} + +
+
+ ); +} + +export default Header; diff --git a/src/screens/professor/Classroom/Header/styles.js b/src/screens/professor/Classroom/Header/styles.js new file mode 100644 index 0000000..03ba4ab --- /dev/null +++ b/src/screens/professor/Classroom/Header/styles.js @@ -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; diff --git a/src/screens/Classroom/PeopleTab/index.js b/src/screens/professor/Classroom/PeopleTab/index.js similarity index 99% rename from src/screens/Classroom/PeopleTab/index.js rename to src/screens/professor/Classroom/PeopleTab/index.js index 3080ee3..9dfde7b 100644 --- a/src/screens/Classroom/PeopleTab/index.js +++ b/src/screens/professor/Classroom/PeopleTab/index.js @@ -1,5 +1,5 @@ import { Avatar, Container, Skeleton, Stack, Typography } from '@mui/material'; -import { createArrayFrom1ToN } from '../../../utils/createArrayFrom1ToN'; +import { createArrayFrom1ToN } from '../../../../utils/createArrayFrom1ToN'; import styles from './styles'; function PeopleTab({ layoutType, peopleTabData }) { diff --git a/src/screens/Classroom/PeopleTab/styles.js b/src/screens/professor/Classroom/PeopleTab/styles.js similarity index 100% rename from src/screens/Classroom/PeopleTab/styles.js rename to src/screens/professor/Classroom/PeopleTab/styles.js diff --git a/src/screens/professor/Classroom/View.js b/src/screens/professor/Classroom/View.js new file mode 100644 index 0000000..59dbd92 --- /dev/null +++ b/src/screens/professor/Classroom/View.js @@ -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 ( + +
+ + + + + + ); +} + +export default View; diff --git a/src/screens/professor/Classroom/index.js b/src/screens/professor/Classroom/index.js new file mode 100644 index 0000000..fa86816 --- /dev/null +++ b/src/screens/professor/Classroom/index.js @@ -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 ( + 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; diff --git a/src/screens/Classroom/styles.js b/src/screens/professor/Classroom/styles.js similarity index 100% rename from src/screens/Classroom/styles.js rename to src/screens/professor/Classroom/styles.js diff --git a/src/screens/professor/Classroom/tabOptions.js b/src/screens/professor/Classroom/tabOptions.js new file mode 100644 index 0000000..fdc3e98 --- /dev/null +++ b/src/screens/professor/Classroom/tabOptions.js @@ -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 }; diff --git a/src/screens/professor/Home/View.js b/src/screens/professor/Home/View.js new file mode 100644 index 0000000..0651aea --- /dev/null +++ b/src/screens/professor/Home/View.js @@ -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 ( + + +

Minhas Turmas

+ + {classrooms === null ? ( + createArrayFrom1ToN(6).map(i => ( + + )) + ) : classrooms.length !== 0 ? ( + classrooms.map(classroom => ( + onClickClassCard(classroom.id)} + /> + )) + ) : ( + +

Nenhuma sala de aula encontrada!

+
+ )} +
+
+ +

Atividades para corrigir

+ + {assignmentsToReview === null ? ( + createArrayFrom1ToN(6).map(i => ( + + )) + ) : assignmentsToReview.length !== 0 ? ( + assignmentsToReview.map(assignment => ( + onClickAssignmentCard(assignment.id)} + /> + )) + ) : ( + +

Nenhuma atividade encontrada!

+
+ )} +
+
+
+ ); + } else if (layoutType === 'mobile') { + return ( + +

Minhas Turmas

+ + {classrooms === null ? ( + createArrayFrom1ToN(6).map(i => ( + + )) + ) : classrooms.length !== 0 ? ( + classrooms.map(classroom => ( + onClickClassCard(classroom.id)} + /> + )) + ) : ( + +

Nenhuma sala de aula encontrada!

+
+ )} +
+

Atividades para corrigir

+ + {assignmentsToReview === null ? ( + createArrayFrom1ToN(6).map(i => ( + + )) + ) : assignmentsToReview.length !== 0 ? ( + assignmentsToReview.map(assignment => ( + onClickAssignmentCard(assignment.id)} + /> + )) + ) : ( + +

Nenhuma atividade encontrada!

+
+ )} +
+
+ ); + } +} + +export default View; diff --git a/src/screens/professor/Home/index.js b/src/screens/professor/Home/index.js new file mode 100644 index 0000000..d16c20d --- /dev/null +++ b/src/screens/professor/Home/index.js @@ -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 ( + + ); +} + +export default Home; diff --git a/src/screens/professor/Home/styles.js b/src/screens/professor/Home/styles.js new file mode 100644 index 0000000..cd02a41 --- /dev/null +++ b/src/screens/professor/Home/styles.js @@ -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; diff --git a/src/screens/Classroom/AnnouncementsTab/index.js b/src/screens/student/Classroom/AnnouncementsTab/index.js similarity index 95% rename from src/screens/Classroom/AnnouncementsTab/index.js rename to src/screens/student/Classroom/AnnouncementsTab/index.js index 5540a94..cded9cc 100644 --- a/src/screens/Classroom/AnnouncementsTab/index.js +++ b/src/screens/student/Classroom/AnnouncementsTab/index.js @@ -8,11 +8,11 @@ import { Stack, Typography, } from '@mui/material'; -import AnnouncementCard from '../../../components/AnnouncementCard'; -import { createArrayFrom1ToN } from '../../../utils/createArrayFrom1ToN'; +import AnnouncementCard from '../../../../components/AnnouncementCard'; +import { createArrayFrom1ToN } from '../../../../utils/createArrayFrom1ToN'; import styles from './styles'; -import jitsiLogo from '../../../assets/jitsi.svg'; +import jitsiLogo from '../../../../assets/jitsi.svg'; function AnnouncementsTab({ layoutType, announcementsTabData, classroom }) { const { container, emptyStateContainer } = styles[layoutType]; @@ -64,7 +64,9 @@ function AnnouncementsTab({ layoutType, announcementsTabData, classroom }) { sx={{ display: 'flex', justifyContent: 'row' }} > Jitsi Meet -

Jitsi

+

+ Sala de aula virtual +