diff options
Diffstat (limited to 'src/screens/student')
-rw-r--r-- | src/screens/student/Classroom/AnnouncementsTab/index.js | 300 | ||||
-rw-r--r-- | src/screens/student/Classroom/AnnouncementsTab/styles.js | 48 | ||||
-rw-r--r-- | src/screens/student/Classroom/AssignmentsTab/index.js | 384 | ||||
-rw-r--r-- | src/screens/student/Classroom/AssignmentsTab/styles.js | 134 | ||||
-rw-r--r-- | src/screens/student/Classroom/Header/index.js | 62 | ||||
-rw-r--r-- | src/screens/student/Classroom/Header/styles.js | 90 | ||||
-rw-r--r-- | src/screens/student/Classroom/PeopleTab/index.js | 244 | ||||
-rw-r--r-- | src/screens/student/Classroom/PeopleTab/styles.js | 116 | ||||
-rw-r--r-- | src/screens/student/Classroom/View.js | 44 | ||||
-rw-r--r-- | src/screens/student/Classroom/index.js | 118 | ||||
-rw-r--r-- | src/screens/student/Classroom/styles.js | 34 | ||||
-rw-r--r-- | src/screens/student/Classroom/tabOptions.js | 16 | ||||
-rw-r--r-- | src/screens/student/Home/View.js | 186 | ||||
-rw-r--r-- | src/screens/student/Home/index.js | 51 | ||||
-rw-r--r-- | src/screens/student/Home/styles.js | 52 |
15 files changed, 1879 insertions, 0 deletions
diff --git a/src/screens/student/Classroom/AnnouncementsTab/index.js b/src/screens/student/Classroom/AnnouncementsTab/index.js new file mode 100644 index 0000000..cded9cc --- /dev/null +++ b/src/screens/student/Classroom/AnnouncementsTab/index.js @@ -0,0 +1,300 @@ +import { + Button, + Card, + Container, + Grid, + Link, + Skeleton, + Stack, + Typography, +} from '@mui/material'; +import AnnouncementCard from '../../../../components/AnnouncementCard'; +import { createArrayFrom1ToN } from '../../../../utils/createArrayFrom1ToN'; + +import styles from './styles'; +import jitsiLogo from '../../../../assets/jitsi.svg'; + +function AnnouncementsTab({ layoutType, announcementsTabData, classroom }) { + const { container, emptyStateContainer } = styles[layoutType]; + + 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' }} + elevation={4} + variant="elevation" + > + <Stack justifyContent="flex-start" spacing={1}> + <Container + disableGutters + sx={{ display: 'flex', justifyContent: 'row' }} + > + <img src={jitsiLogo} alt="Jitsi Meet" /> + <h3 style={{ fontWeight: 500 }}> + Sala de aula virtual + </h3> + </Container> + + <Button + variant="contained" + href="https://google.com" + target="__blank" + > + Entrar + </Button> + </Stack> + </Card> + <Card + sx={{ width: '100%', padding: '20px' }} + 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' }} + 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> + </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" + > + {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' }} + elevation={4} + variant="elevation" + > + <Stack justifyContent="flex-start" spacing={1}> + <Container + disableGutters + sx={{ display: 'flex', justifyContent: 'row' }} + > + <img src={jitsiLogo} alt="Jitsi Meet" /> + <h3 style={{ fontWeight: 500 }}>Sala de aula virtual</h3> + </Container> + + <Button + variant="contained" + href="https://google.com" + target="__blank" + > + Entrar + </Button> + </Stack> + </Card> + <Card + sx={{ width: '100%', padding: '20px' }} + 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' }} + 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" + > + {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; diff --git a/src/screens/student/Classroom/AnnouncementsTab/styles.js b/src/screens/student/Classroom/AnnouncementsTab/styles.js new file mode 100644 index 0000000..d7d218a --- /dev/null +++ b/src/screens/student/Classroom/AnnouncementsTab/styles.js @@ -0,0 +1,48 @@ +// ========== 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, +}; + +const styles = { desktop, mobile, unset }; +export default styles; diff --git a/src/screens/student/Classroom/AssignmentsTab/index.js b/src/screens/student/Classroom/AssignmentsTab/index.js new file mode 100644 index 0000000..19757b4 --- /dev/null +++ b/src/screens/student/Classroom/AssignmentsTab/index.js @@ -0,0 +1,384 @@ +import { Container, Link, Skeleton, Stack, Typography } from '@mui/material'; +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} 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> + <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 sx={assignmentTypography} variant="h5"> + <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="h5"> + <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> + <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; diff --git a/src/screens/student/Classroom/AssignmentsTab/styles.js b/src/screens/student/Classroom/AssignmentsTab/styles.js new file mode 100644 index 0000000..8c97bd3 --- /dev/null +++ b/src/screens/student/Classroom/AssignmentsTab/styles.js @@ -0,0 +1,134 @@ +// ========== Desktop ========== +const desktopExternalContainer = { + marginTop: '50px', + height: '100vh', +}; + +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', +}; + +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/student/Classroom/Header/index.js b/src/screens/student/Classroom/Header/index.js new file mode 100644 index 0000000..6a4a1a8 --- /dev/null +++ b/src/screens/student/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 ? ( + <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="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; diff --git a/src/screens/student/Classroom/Header/styles.js b/src/screens/student/Classroom/Header/styles.js new file mode 100644 index 0000000..58d19aa --- /dev/null +++ b/src/screens/student/Classroom/Header/styles.js @@ -0,0 +1,90 @@ +// ========== Desktop ========== +const desktopTitle = { + fontWeight: 500, +}; + +const desktopPaper = classColor => ({ + width: '100%', + borderTop: `5px solid ${classColor}`, + padding: '20px', +}); + +const desktopTabs = { + marginLeft: '-20px', + marginRight: '-20px', + marginBottom: '-20px', + 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: '10px', +}); + +const mobileTabs = { + marginLeft: '-10px', + marginRight: '-10px', + marginBottom: '-10px', + 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/student/Classroom/PeopleTab/index.js b/src/screens/student/Classroom/PeopleTab/index.js new file mode 100644 index 0000000..9dfde7b --- /dev/null +++ b/src/screens/student/Classroom/PeopleTab/index.js @@ -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; diff --git a/src/screens/student/Classroom/PeopleTab/styles.js b/src/screens/student/Classroom/PeopleTab/styles.js new file mode 100644 index 0000000..30668db --- /dev/null +++ b/src/screens/student/Classroom/PeopleTab/styles.js @@ -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; diff --git a/src/screens/student/Classroom/View.js b/src/screens/student/Classroom/View.js new file mode 100644 index 0000000..675e697 --- /dev/null +++ b/src/screens/student/Classroom/View.js @@ -0,0 +1,44 @@ +import { Container } from '@mui/material'; + +import Header from './Header'; +import AnnouncementsTab from './AnnouncementsTab'; +import AssignmentsTab from './AssignmentsTab'; +import PeopleTab from './PeopleTab'; + +import styles from './styles'; + +function View({ + layoutType, + classroom, + selectedTabOption, + onSelectTabOption, + announcementsTabData, + assignmentsTabData, + peopleTabData, + isLoading, +}) { + const { container } = styles[layoutType]; + return ( + <Container disableGutters sx={container}> + <Header + layoutType={layoutType} + classroom={classroom} + selectedTabOption={selectedTabOption} + onSelectTabOption={onSelectTabOption} + isLoading={isLoading} + /> + <AnnouncementsTab + layoutType={layoutType} + announcementsTabData={announcementsTabData} + classroom={classroom && classroom} + /> + <AssignmentsTab + layoutType={layoutType} + assignmentsTabData={assignmentsTabData} + /> + <PeopleTab layoutType={layoutType} peopleTabData={peopleTabData} /> + </Container> + ); +} + +export default View; diff --git a/src/screens/student/Classroom/index.js b/src/screens/student/Classroom/index.js new file mode 100644 index 0000000..5201b49 --- /dev/null +++ b/src/screens/student/Classroom/index.js @@ -0,0 +1,118 @@ +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 } = useUser(); + const [classroom, setClassroom] = useState(null); + const [tabData, setTabData] = useState(null); + const [selectedTabOption, setSelectedTabOption] = useState( + TAB_OPTIONS.announcements.value + ); + + 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]); + + 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]); + + 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; + default: + console.log('Invalid tab option'); + } + } + getSelectedTabData(); + }, [ + selectedTabOption, + params, + fetchAndPopulateAnnouncementsTabData, + fetchAndPopulateAssignmentsTabData, + fetchAndPopulatePeopleTabData, + ]); + + 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; + } + } + + getClassroomById(params.id); + updateDocumentTitle(); + }, [userService, userService.fetchClassroomById, params, classroom]); + + 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' } + } + isLoading={tabData && tabData.state === 'loading'} + /> + ); +} + +export default Classroom; diff --git a/src/screens/student/Classroom/styles.js b/src/screens/student/Classroom/styles.js new file mode 100644 index 0000000..fe6018c --- /dev/null +++ b/src/screens/student/Classroom/styles.js @@ -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; diff --git a/src/screens/student/Classroom/tabOptions.js b/src/screens/student/Classroom/tabOptions.js new file mode 100644 index 0000000..48fb518 --- /dev/null +++ b/src/screens/student/Classroom/tabOptions.js @@ -0,0 +1,16 @@ +const TAB_OPTIONS = { + announcements: { + value: 0, + lable: 'Comunicados', + }, + assignments: { + value: 1, + lable: 'Atividades', + }, + people: { + value: 2, + lable: 'Pessoas', + }, +}; + +export { TAB_OPTIONS }; diff --git a/src/screens/student/Home/View.js b/src/screens/student/Home/View.js new file mode 100644 index 0000000..27b0f78 --- /dev/null +++ b/src/screens/student/Home/View.js @@ -0,0 +1,186 @@ +import { Container, Grid, Skeleton, Stack } from '@mui/material'; + +import ClassCard from '../../../components/ClassCard'; +import AssignmentCard from '../../../components/AssignmentCard'; + +import styles from './styles'; +import { createArrayFrom1ToN } from '../../../utils/createArrayFrom1ToN'; + +function View({ + layoutType, + classrooms, + assignments, + onClickClassCard, + onClickAssignmentCard, +}) { + const { container, divider, assignmentsStack } = styles[layoutType]; + + if (layoutType === 'desktop') { + return ( + <Grid sx={container} container spacing={2}> + <Grid item xs={8}> + <h1>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} + 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</h1> + <h2>Atribuídas</h2> + <Stack + sx={assignmentsStack} + alignItems="end" + flexWrap="wrap" + direction="row" + gap="30px" + > + {assignments === null ? ( + createArrayFrom1ToN(6).map(i => ( + <Skeleton + key={i} + variant="rectangular" + width="35em" + height={145} + /> + )) + ) : assignments.length !== 0 ? ( + assignments.map(assignment => ( + <AssignmentCard + key={assignment.title} + title={assignment.title} + classrooms={assignment.classrooms} + dueDate={assignment.dueDate} + scores={assignment.scores} + layoutType={layoutType} + 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>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} + layoutType={layoutType} + onClick={() => onClickClassCard(classroom.id)} + /> + )) + ) : ( + <Container disableGutters> + <p>Nenhuma sala de aula encontrada!</p> + </Container> + )} + </Stack> + <h1 style={divider}>Atividades</h1> + <h2>Atribuídas</h2> + <Stack + sx={assignmentsStack} + alignItems="center" + justifyContent="center" + flexWrap="wrap" + direction="row" + gap="30px" + > + {assignments === null ? ( + createArrayFrom1ToN(6).map(i => ( + <Skeleton + key={i} + variant="rectangular" + width="100%" + height={190} + /> + )) + ) : assignments.length !== 0 ? ( + assignments.map(assignment => ( + <AssignmentCard + key={assignment.title} + title={assignment.title} + classrooms={assignment.classrooms} + dueDate={assignment.dueDate} + scores={assignment.scores} + layoutType={layoutType} + onClick={() => onClickAssignmentCard(assignment.id)} + /> + )) + ) : ( + <Container disableGutters> + <p>Nenhuma atividade encontrada!</p> + </Container> + )} + </Stack> + </Stack> + ); + } +} + +export default View; diff --git a/src/screens/student/Home/index.js b/src/screens/student/Home/index.js new file mode 100644 index 0000000..75d734e --- /dev/null +++ b/src/screens/student/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 [assignments, setAssignments] = useState(null); + + useEffect(() => { + async function getClassrooms() { + const result = await userService.fetchClassrooms(); + setClassrooms(result.data); + } + getClassrooms(); + }, [userService, userService.fetchClassrooms]); + + useEffect(() => { + async function getAssignments() { + const result = await userService.fetchAllAssignments(); + setAssignments(result.data); + } + getAssignments(); + }, [userService, userService.fetchAllAssignments]); + + const onClickClassCard = id => { + navigate(`/class/${id}`); + }; + + const onClickAssignmentCard = id => { + navigate(`/assignment/${id}`); + }; + + return ( + <View + layoutType={layoutType} + classrooms={classrooms} + assignments={assignments} + onClickClassCard={onClickClassCard} + onClickAssignmentCard={onClickAssignmentCard} + /> + ); +} + +export default Home; diff --git a/src/screens/student/Home/styles.js b/src/screens/student/Home/styles.js new file mode 100644 index 0000000..4ddefb9 --- /dev/null +++ b/src/screens/student/Home/styles.js @@ -0,0 +1,52 @@ +// ========== Desktop ========== +const desktopContainer = { + height: '100vh', + margin: 0, +}; + +const desktopDivider = { + borderLeft: '4px solid #CFCFCF', +}; + +const desktopAssignmentsStack = { + paddingBottom: '100px', +}; + +const desktop = { + container: desktopContainer, + divider: desktopDivider, + assignmentsStack: desktopAssignmentsStack, +}; + +// ========== Mobile ========== +const mobileContainer = { + height: 'inherit', + width: '100%', + padding: '10px 20px ', + margin: 0, +}; + +const mobileDivider = { + borderTop: '2px solid #CFCFCF', + paddingTop: '15px', +}; + +const mobileAssignmentsStack = { + paddingBottom: '100px', +}; + +const mobile = { + container: mobileContainer, + divider: mobileDivider, + assignmentsStack: mobileAssignmentsStack, +}; + +// ========== Unset ========== +const unset = { + container: null, + divider: null, + assignmentsStack: null, +}; + +const styles = { desktop, mobile, unset }; +export default styles; |