Skip to content

Commit 955e8a6

Browse files
Data loading for tasks in class, go to /[classID]/tasks to view the tasks of the class
Also added animation when card is clicked
1 parent 9423fd2 commit 955e8a6

20 files changed

+894
-132
lines changed

components/ProminentAppBar.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function ElevationScroll(props: { children: ReactElement }) {
2727
});
2828
}
2929

30-
export default function ProminentAppBar(props: {appName: string, children?: ReactElement}) {
30+
export default function ProminentAppBar(props: {title: string, children?: ReactElement}) {
3131
return (
3232
<Box sx={{ flexGrow: 1, top: -64, position: 'sticky', zIndex: t => t.zIndex.appBar }}>
3333
<ElevationScroll>
@@ -39,7 +39,7 @@ export default function ProminentAppBar(props: {appName: string, children?: Reac
3939
component='div'
4040
sx={{ flexGrow: 1, alignSelf: 'flex-end' }}
4141
>
42-
{props.appName}
42+
{props.title}
4343
</Typography>
4444
{props.children}
4545
</StyledToolbar>

components/TaskCard.tsx

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {
2+
Button,
3+
Card,
4+
CardActionArea,
5+
CardActions,
6+
CardContent,
7+
Modal,
8+
Typography,
9+
Box,
10+
Divider,
11+
IconButton, Tooltip
12+
} from '@mui/material';
13+
import { AnimatePresence, AnimateSharedLayout, motion } from 'framer-motion';
14+
import { ITask } from '../types/ITask';
15+
import { styled } from '@mui/material/styles';
16+
import { EditRounded } from '@mui/icons-material';
17+
18+
const Item = styled(Card)(({ theme }) => ({
19+
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
20+
...theme.typography.body2,
21+
color: theme.palette.text.secondary,
22+
borderRadius: 7
23+
}));
24+
25+
const MotionItem = motion(Item);
26+
27+
export default function TaskCard(props: {expanded: boolean, task: ITask, onSelect: (id: string | null) => void}) {
28+
const
29+
{expanded, task, onSelect} = props,
30+
id = task._id.toString();
31+
32+
return <AnimateSharedLayout>
33+
{
34+
!expanded && <MotionItem layoutId={id} variant={'outlined'}>
35+
<CardActionArea onClick={() => onSelect(id)}>
36+
<CardContent sx={{py: 1.5, pt: 1.25}}>
37+
<Typography gutterBottom variant='h6' component='div' color={'text.primary'}>{task.title}</Typography>
38+
<Typography variant='body2' color='text.secondary'>{task.content}</Typography>
39+
</CardContent>
40+
</CardActionArea>
41+
</MotionItem>
42+
}
43+
<AnimatePresence>
44+
<Modal open={expanded} onClose={() => onSelect(null)} sx={{
45+
display: 'flex',
46+
alignItems: 'center',
47+
justifyContent: 'center',
48+
}}>
49+
<MotionItem
50+
sx={{
51+
width: 'min(600px, calc(100vw - 1rem))',
52+
outline: 'none',
53+
borderRadius: 2
54+
}}
55+
layoutId={id}
56+
>
57+
<CardContent sx={{py: 1.5, pt: 1.25}}>
58+
<Typography gutterBottom variant='h5' component='div' color={'text.primary'}>{task.title}</Typography>
59+
<Typography variant='body2' color={'text.secondary'}>{task.content}</Typography>
60+
</CardContent>
61+
<Divider />
62+
<CardActions>
63+
<Tooltip title={'Edit'}>
64+
<IconButton><EditRounded sx={{color: t => t.palette.text.secondary}} /></IconButton>
65+
</Tooltip>
66+
<Box flexGrow={1} />
67+
<Button variant={'outlined'} onClick={() => onSelect(null)}>Close</Button>
68+
</CardActions>
69+
</MotionItem>
70+
</Modal>
71+
</AnimatePresence>
72+
</AnimateSharedLayout>
73+
}

components/TaskCreator.tsx

+123-14
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import { styled } from '@mui/material/styles';
2-
import { Box, Button, ClickAwayListener, Collapse, Container, InputBase, Paper, TextareaAutosize } from '@mui/material';
2+
import {
3+
Box,
4+
Button,
5+
ButtonGroup,
6+
ClickAwayListener,
7+
Collapse,
8+
Container,
9+
Divider, Grow, IconButton,
10+
InputBase,
11+
ListItemIcon,
12+
ListItemText,
13+
ListSubheader,
14+
Menu,
15+
MenuItem,
16+
Paper,
17+
TextareaAutosize, Tooltip, Typography
18+
} from '@mui/material';
319
import { useState } from 'react';
20+
import { CampaignRounded, ExpandMoreRounded, GroupsRounded, PersonRounded } from '@mui/icons-material';
21+
import { TaskVisibility } from './types';
22+
import { User } from 'next-auth';
423

524
const StyledAutoresizeTextarea = styled(TextareaAutosize)(({ theme }) => ({
625
backgroundColor: 'transparent',
@@ -9,35 +28,66 @@ const StyledAutoresizeTextarea = styled(TextareaAutosize)(({ theme }) => ({
928
outline: 'none',
1029
border: 'none',
1130
padding: '.75rem 1rem',
31+
margin: 0,
1232
resize: 'none',
1333
...theme.typography.body1,
34+
'&::-webkit-scrollbar': {
35+
width: '.5rem',
36+
backgroundColor: 'transparent'
37+
},
38+
'&::-webkit-scrollbar-thumb': {
39+
backgroundColor: theme.palette.divider
40+
}
1441
}));
1542

1643
const TitleInput = styled(InputBase)(({ theme }) => ({
1744
backgroundColor: 'transparent',
1845
color: theme.palette.text.primary,
1946
flexGrow: 1,
2047
width: '100%',
21-
padding: '1rem 1rem 0 1rem',
48+
padding: '.8rem 1rem .5rem 1rem',
2249
...theme.typography.h6,
23-
'&>input': {
24-
padding: 0
25-
}
50+
'&>input': { padding: 0 }
51+
}));
52+
53+
const CreatorContainer = styled(Paper)(({ theme }) => ({
54+
border: `1px solid ${theme.palette.divider}`,
55+
margin: '1rem auto',
56+
display: 'flex',
57+
flexDirection: 'column',
58+
borderRadius: 24
59+
}));
60+
61+
const MenuHeader = styled(ListSubheader)(({ theme }) => ({
62+
backgroundColor: 'transparent',
63+
lineHeight: '1em',
64+
margin: '.4rem 0 .6rem',
65+
paddingTop: 8,
2666
}));
2767

28-
export default function TaskCreator() {
68+
export default function TaskCreator(props: {isAdmin: boolean, isSuperAdmin: boolean}) {
2969
const
3070
[inputFocused, setInputFocused] = useState(false),
3171
[title, setTitle] = useState(''),
3272
[content, setContent] = useState(''),
33-
canShrink = title.length === 0 && content.length === 0 && !inputFocused;
73+
[optAnchor, setOptAnchor] = useState<null | HTMLElement>(null),
74+
[visibility, setVisibility] = useState<TaskVisibility>(TaskVisibility.Private),
75+
[optionsExpanded, setOptionsExpanded] = useState(false),
76+
shrunk = title.length === 0 && content.length === 0 && !inputFocused;
77+
78+
const handleClose = (op: TaskVisibility | null = null) => {
79+
return () => {
80+
setOptAnchor(null);
81+
if (op) setVisibility(op);
82+
}
83+
}
3484

3585
return <ClickAwayListener onClickAway={() => setInputFocused(false)}>
3686
<Container maxWidth={'sm'} onClick={() => setInputFocused(true)}>
37-
<Paper variant={'outlined'}
38-
sx={{mx: 'auto', my: 2, display: 'flex', flexDirection: 'column', borderRadius: 2}}>
39-
<Collapse in={!canShrink}>
87+
<CreatorContainer variant={shrunk ? 'outlined' : 'elevation'}>
88+
<Collapse in={!shrunk}>
4089
<TitleInput placeholder={'Title'} onChange={e => setTitle(e.target.value)} value={title} />
90+
<Divider sx={{borderStyle: 'dashed'}} />
4191
</Collapse>
4292

4393
<StyledAutoresizeTextarea
@@ -46,13 +96,72 @@ export default function TaskCreator() {
4696
onInput={e => setContent(e.currentTarget.value)}
4797
/>
4898

49-
<Collapse in={!canShrink}>
50-
<Box px={1.4} pb={1.25} display={'flex'}>
99+
<Collapse in={!shrunk}>
100+
<Collapse in={optionsExpanded}>
101+
<Divider sx={{borderStyle: 'dashed'}} />
102+
<Box px={2}>
103+
<Typography variant={'overline'}>Options</Typography>
104+
</Box>
105+
</Collapse>
106+
107+
<Divider sx={{borderStyle: 'dashed'}} />
108+
109+
<Box px={1.3} py={1.25} display={'flex'} alignItems={'center'} gap={1}>
110+
<IconButton size={'small'} onClick={() => setOptionsExpanded(!optionsExpanded)}>
111+
<ExpandMoreRounded sx={{
112+
transform: `rotate(${optionsExpanded ? 180 : 0}deg)`,
113+
transition: t => t.transitions.create(['transform'])
114+
}} />
115+
</IconButton>
116+
<Grow in={!optionsExpanded}>
117+
<Typography variant={'caption'} lineHeight={'1.2em'}>More options</Typography>
118+
</Grow>
119+
51120
<Box flexGrow={1} />
52-
<Button size={'small'}>Add</Button>
121+
122+
<ButtonGroup variant={'contained'} size={'small'}>
123+
<Button>Add</Button>
124+
<Button
125+
sx={{pl: .3, pr: .7, minWidth: '0!important'}}
126+
aria-haspopup
127+
aria-controls={!!optAnchor ? 'add-ops' : undefined}
128+
onClick={e => setOptAnchor(e.currentTarget)}
129+
><ExpandMoreRounded /></Button>
130+
</ButtonGroup>
131+
<Menu
132+
id={'add-ops'}
133+
anchorEl={optAnchor}
134+
open={!!optAnchor}
135+
onClose={handleClose()}
136+
MenuListProps={{ 'aria-labelledby': 'basic-button', dense: true }}
137+
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
138+
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
139+
>
140+
<MenuHeader>Visibility</MenuHeader>
141+
<Divider />
142+
<MenuItem onClick={handleClose(TaskVisibility.Private)}
143+
selected={visibility === TaskVisibility.Private}>
144+
<ListItemIcon><PersonRounded /></ListItemIcon>
145+
<ListItemText primary={'Private'} secondary={'Only visible to you'} />
146+
</MenuItem>
147+
{
148+
props.isAdmin && <MenuItem onClick={handleClose(TaskVisibility.Class)}
149+
selected={visibility === TaskVisibility.Class}>
150+
<ListItemIcon><GroupsRounded /></ListItemIcon>
151+
<ListItemText primary={'Class'} secondary={'Visible to class members'} />
152+
</MenuItem>
153+
}
154+
{
155+
props.isSuperAdmin && <MenuItem onClick={handleClose(TaskVisibility.Announcement)}
156+
selected={visibility === TaskVisibility.Announcement}>
157+
<ListItemIcon><CampaignRounded /></ListItemIcon>
158+
<ListItemText primary={'Announcement'} secondary={''} />
159+
</MenuItem>
160+
}
161+
</Menu>
53162
</Box>
54163
</Collapse>
55-
</Paper>
164+
</CreatorContainer>
56165
</Container>
57166
</ClickAwayListener>;
58167
}

components/TaskList.tsx

+24-22
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,30 @@
11
import { Masonry } from '@mui/lab';
22
import { styled } from '@mui/material/styles';
3-
import { Container, Paper } from '@mui/material';
3+
import { Card, CardActionArea, CardContent, Container, Modal, Typography } from '@mui/material';
4+
import { ITask } from '../types/ITask';
5+
import { useState } from 'react';
6+
import TaskCard from './TaskCard';
47

5-
const heights = [150, 30, 90, 70, 110, 150, 130, 80, 50, 90, 100, 150, 30, 50, 80];
68

7-
const Item = styled(Paper)(({ theme }) => ({
8-
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
9-
...theme.typography.body2,
10-
padding: theme.spacing(0.5),
11-
textAlign: 'center',
12-
color: theme.palette.text.secondary,
13-
borderRadius: 7
14-
}));
9+
export default function TaskList(props: { tasks: ITask[] }) {
10+
const [selTask, setSelTask] = useState<string | null>(null);
1511

16-
export default function TaskList() {
17-
return <Container maxWidth={'lg'} sx={{p: .5}}>
18-
<Masonry
19-
sx={{m: 0}}
20-
spacing={1}
21-
columns={{ 'lg': 4, 'md': 3, 'sm': 2, 'xs': 1 }}
22-
>
23-
{heights.map((height, index) =>
24-
<Item key={index} sx={{ height }} variant={'outlined'}>{index + 1}</Item>
25-
)}
26-
</Masonry>
27-
</Container>
12+
return <>
13+
<Container maxWidth={'lg'} sx={{p: .5}}>
14+
<Masonry
15+
sx={{m: 0}}
16+
spacing={1}
17+
columns={{ 'lg': 4, 'md': 3, 'sm': 2, 'xs': 1 }}
18+
>
19+
{ props.tasks.map(t =>
20+
<TaskCard
21+
key={t._id.toString()}
22+
expanded={selTask === t._id.toString()}
23+
task={t}
24+
onSelect={id => setSelTask(id)}
25+
/>
26+
) }
27+
</Masonry>
28+
</Container>
29+
</>
2830
}

components/types.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const enum TaskVisibility {
2+
Private = "PRIVATE",
3+
Class = "CLASS",
4+
Announcement = "ANNOUNCEMENT",
5+
}

lib/containsObjectId.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { ObjectId } from 'bson';
2+
3+
export default function containsObjectId (arr: ObjectId[], id: string): boolean {
4+
return arr.some(item => item.toHexString() === id);
5+
}

lib/makeTheme.ts

+10
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ export default function makeTheme() {
3939
},
4040
defaultProps: { variant: 'contained', }
4141
},
42+
MuiListItemText: {
43+
styleOverrides: {
44+
secondary: { fontSize: '.8em' }
45+
}
46+
},
47+
MuiList: {
48+
styleOverrides: {
49+
root: { padding: 0 }
50+
}
51+
},
4252
},
4353
})
4454
}

next.config.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
const { withSuperjson } = require('next-superjson')
2+
13
/** @type {import('next').NextConfig} */
24
const nextConfig = {
35
reactStrictMode: true,
6+
productionSourceMap: false,
7+
swcMinify: true
48
}
59

6-
module.exports = nextConfig
10+
module.exports = withSuperjson()(nextConfig);

package.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,14 @@
1717
"@mui/lab": "^5.0.0-alpha.76",
1818
"@mui/material": "^5.6.0",
1919
"@next-auth/mongodb-adapter": "^1.0.3",
20+
"framer-motion": "^6.2.9",
2021
"mongodb": "^4.5.0",
2122
"next": "12.1.4",
2223
"next-auth": "^4.3.1",
24+
"next-superjson": "^0.0.2",
2325
"react": "18.0.0",
24-
"react-dom": "18.0.0"
26+
"react-dom": "18.0.0",
27+
"superjson": "^1.8.1"
2528
},
2629
"devDependencies": {
2730
"@types/node": "17.0.23",

0 commit comments

Comments
 (0)