Skip to content

Commit 52e9fbc

Browse files
authored
feat: Add Activity & Tasks section to Tracker Dawer (#215)
* created activity & tasks section of tracker drawer * updated node version in frontendtests
1 parent 9c49e24 commit 52e9fbc

8 files changed

Lines changed: 12648 additions & 24637 deletions

File tree

.github/workflows/frontendtests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
working-directory: client
1919
strategy:
2020
matrix:
21-
node-version: [14.x, 16.x, 18.x]
21+
node-version: [18.x, 20.x]
2222
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
2323

2424
steps:

client/package-lock.json

Lines changed: 12248 additions & 24636 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
"@emotion/styled": "^11.10.6",
99
"@fontsource/poppins": "^4.5.10",
1010
"@mui/icons-material": "^5.11.0",
11+
"@mui/lab": "^5.0.0-alpha.169",
1112
"@mui/material": "^5.11.16",
13+
"@mui/x-date-pickers": "^5.0.20",
1214
"axios": "^1.3.4",
1315
"date-fns": "^2.29.3",
1416
"dayjs": "^1.11.10",
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import AddRoundedIcon from '@mui/icons-material/AddRounded';
2+
import {
3+
Timeline,
4+
TimelineConnector,
5+
TimelineContent,
6+
TimelineDot,
7+
TimelineItem,
8+
TimelineOppositeContent,
9+
TimelineSeparator,
10+
} from '@mui/lab';
11+
import { Button } from '@mui/material';
12+
import React, { useEffect, useState } from 'react';
13+
14+
import { getActivitiesByTrackedInternshipId } from '../utils/api';
15+
import { addActivity } from '../utils/api';
16+
import TrackedActivityRow from './TrackedActivityRow';
17+
18+
const TrackedActivity = ({ trackedInternshipId }) => {
19+
const [activity, setActivity] = useState([]);
20+
const [isActivityUpdated, setIsActivityUpdated] = useState(true);
21+
22+
const addEvent = async () => {
23+
try {
24+
// By setting the title to an empty string, the added event will default to edit mode
25+
await addActivity(trackedInternshipId, '', null);
26+
setIsActivityUpdated(true);
27+
} catch (err) {
28+
console.error('Error adding activity:', err);
29+
}
30+
};
31+
32+
useEffect(() => {
33+
const fetchActivities = async () => {
34+
if (trackedInternshipId) {
35+
try {
36+
const allActivity = await getActivitiesByTrackedInternshipId(
37+
trackedInternshipId
38+
);
39+
setActivity(allActivity);
40+
} catch (error) {
41+
console.error('Error getting internship activity.', error);
42+
}
43+
}
44+
};
45+
46+
if (isActivityUpdated) {
47+
fetchActivities();
48+
setIsActivityUpdated(false);
49+
}
50+
}, [trackedInternshipId, isActivityUpdated]);
51+
52+
return (
53+
<>
54+
{activity.length > 0 && (
55+
<Timeline sx={{ p: 0, m: 0 }}>
56+
{activity.map((event, index) => (
57+
<TimelineItem sx={{ minHeight: '3.2rem' }} key={event.id}>
58+
<TimelineOppositeContent
59+
sx={{ flex: 0 }}
60+
></TimelineOppositeContent>
61+
<TimelineSeparator>
62+
<TimelineDot
63+
variant="outlined"
64+
sx={{
65+
borderColor: 'tertiary.main',
66+
backgroundColor:
67+
event.date == null || new Date(event.date) > new Date()
68+
? 'white'
69+
: 'background.dark',
70+
width: '1.1rem',
71+
height: '1.1rem',
72+
marginY: '.5rem',
73+
}}
74+
/>
75+
{index < activity.length - 1 && (
76+
<TimelineConnector
77+
sx={{
78+
borderColor: 'background.dark',
79+
borderStyle: 'dashed',
80+
backgroundColor: 'white',
81+
borderWidth: 1,
82+
}}
83+
/>
84+
)}
85+
</TimelineSeparator>
86+
<TimelineContent sx={{ m: 0, p: 0 }}>
87+
<TrackedActivityRow
88+
event={event}
89+
onEventUpdated={() => setIsActivityUpdated(true)}
90+
defaultEditMode={!event.title}
91+
/>
92+
</TimelineContent>
93+
</TimelineItem>
94+
))}
95+
</Timeline>
96+
)}
97+
<Button
98+
variant="rounded"
99+
color="primary"
100+
onClick={addEvent}
101+
startIcon={<AddRoundedIcon />}
102+
sx={{ mb: '1rem', mt: '.5rem' }}
103+
>
104+
Add task
105+
</Button>
106+
</>
107+
);
108+
};
109+
110+
export default TrackedActivity;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';
2+
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
3+
import EditRoundedIcon from '@mui/icons-material/EditRounded';
4+
import { IconButton, Stack, TextField, Typography } from '@mui/material';
5+
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
6+
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
7+
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
8+
import React, { useState } from 'react';
9+
10+
import { deleteActivity, editActivity } from '../utils/api';
11+
12+
const TEXT_FIELD_STYLE = {
13+
'& .MuiInputBase-input': {
14+
fontSize: '0.9rem',
15+
fontWeight: 200,
16+
},
17+
'& .MuiInputLabel-root': {
18+
fontSize: '0.9rem !important',
19+
fontWeight: 200,
20+
},
21+
'& .MuiInputLabel-shrink': {
22+
fontWeight: 200,
23+
},
24+
};
25+
26+
const TrackedActivityRow = ({
27+
event,
28+
onEventUpdated,
29+
defaultEditMode = false,
30+
}) => {
31+
const [isEditMode, setIsEditMode] = useState(defaultEditMode);
32+
const [title, setTitle] = useState(event.title);
33+
const [date, setDate] = useState(event.date);
34+
35+
const updateEvent = async () => {
36+
try {
37+
await editActivity(event.id, title, date);
38+
setIsEditMode(false);
39+
} catch (err) {
40+
console.error('Error updating activity:', err);
41+
}
42+
onEventUpdated();
43+
};
44+
45+
const deleteEvent = async () => {
46+
try {
47+
await deleteActivity(event.id);
48+
} catch (err) {
49+
console.error('Error deleting activity:', err);
50+
}
51+
onEventUpdated();
52+
};
53+
54+
return (
55+
<LocalizationProvider dateAdapter={AdapterDateFns}>
56+
<Stack
57+
direction="row"
58+
justifyContent="space-between"
59+
alignItems="center"
60+
paddingBottom="1rem"
61+
paddingLeft="1rem"
62+
borderRadius="1rem"
63+
spacing={4}
64+
>
65+
{isEditMode ? (
66+
<TextField
67+
label="Activity"
68+
size="small"
69+
fullWidth
70+
value={title}
71+
onChange={(event) => setTitle(event.target.value)}
72+
sx={TEXT_FIELD_STYLE}
73+
/>
74+
) : (
75+
<Typography variant="body3" fontWeight={200}>
76+
{event.title}
77+
</Typography>
78+
)}
79+
<Stack direction="row" alignItems="center" spacing={2} py={0}>
80+
{isEditMode ? (
81+
<DatePicker
82+
label="Date"
83+
value={date}
84+
onChange={(newDate) => setDate(newDate)}
85+
renderInput={(params) => (
86+
<TextField
87+
{...params}
88+
size="small"
89+
sx={{
90+
...TEXT_FIELD_STYLE,
91+
minWidth: '9.5rem',
92+
}}
93+
/>
94+
)}
95+
/>
96+
) : (
97+
<Typography variant="body3" color="text.light" fontWeight={200}>
98+
{event.date ? new Date(event.date).toLocaleDateString() : ''}
99+
</Typography>
100+
)}
101+
<Stack direction="row" py={0}>
102+
{isEditMode ? (
103+
<IconButton onClick={updateEvent}>
104+
<CheckCircleRoundedIcon fontSize="small" />
105+
</IconButton>
106+
) : (
107+
<IconButton onClick={() => setIsEditMode(true)}>
108+
<EditRoundedIcon fontSize="small" />
109+
</IconButton>
110+
)}
111+
<IconButton onClick={deleteEvent}>
112+
<DeleteRoundedIcon fontSize="small" />
113+
</IconButton>
114+
</Stack>
115+
</Stack>
116+
</Stack>
117+
</LocalizationProvider>
118+
);
119+
};
120+
121+
export default TrackedActivityRow;

client/src/components/TrackerDrawer.jsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import InternshipTag from './InternshipTag';
2525
import JobDescription from './JobDescription';
2626
import Loading from './Loading';
2727
import StatusDropdown from './StatusDropdown';
28+
import TrackedActivity from './TrackedActivity';
2829

2930
const TrackerDrawer = ({
3031
trackedInternshipId,
@@ -172,6 +173,14 @@ const TrackerDrawer = ({
172173
requirements={internshipInfo.jobInfo.jobReqs}
173174
responsibilities={internshipInfo.jobInfo.jobResp}
174175
/>
176+
<Typography
177+
variant="h6"
178+
paddingTop="1.5rem"
179+
paddingBottom="1rem"
180+
>
181+
Activity & Tasks
182+
</Typography>
183+
<TrackedActivity trackedInternshipId={trackedInternshipId} />
175184
</Box>
176185
</Box>
177186
<Box bgcolor="background.main" padding="1rem 0.8rem 1rem 1.2rem">

client/src/utils/api.js

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,104 @@ export const addNote = async (trackedInternshipId, message) => {
160160
return newNote;
161161
};
162162

163+
/**
164+
* Get all activities from a tracked internship
165+
* @param trackedInternshipId tracked internship id
166+
* @returns an array of activities
167+
*/
168+
export const getActivitiesByTrackedInternshipId = async (
169+
trackedInternshipId
170+
) => {
171+
const trackedInternship = mockTrackerData.find(
172+
(trackedInternship) => trackedInternship.id === trackedInternshipId
173+
);
174+
175+
if (!trackedInternship)
176+
throw new Error(
177+
`No tracked internship associated with id: ${trackedInternshipId}.`
178+
);
179+
180+
// Sort activity in chronological order
181+
const sortedActivity = [...trackedInternship.activity].sort((a, b) => {
182+
const timeA = a.date ? new Date(a.date).getTime() : Infinity;
183+
const timeB = b.date ? new Date(b.date).getTime() : Infinity;
184+
return timeA - timeB;
185+
});
186+
187+
return sortedActivity;
188+
};
189+
190+
/**
191+
* Add an activity to a tracked internship
192+
* @param trackedInternshipId tracked internship id
193+
* @param title activity title
194+
* @param date activity date (can be null)
195+
* @returns newly created activity
196+
*/
197+
export const addActivity = async (trackedInternshipId, title, date = null) => {
198+
const trackedInternship = mockTrackerData.find(
199+
(ti) => ti.id === trackedInternshipId
200+
);
201+
202+
if (!trackedInternship)
203+
throw new Error(
204+
`No tracked internship found with id: ${trackedInternshipId}`
205+
);
206+
207+
const newActivityId =
208+
trackedInternship.activity.length > 0
209+
? Math.max(...trackedInternship.activity.map((a) => a.id)) + 1
210+
: 0;
211+
212+
const newActivity = { id: newActivityId, title, date };
213+
trackedInternship.activity.push(newActivity);
214+
return newActivity;
215+
};
216+
217+
/**
218+
* Edit an activity by activity ID
219+
* @param activityId activity id
220+
* @param title new title
221+
* @param date new date
222+
* @returns updated activity
223+
*/
224+
export const editActivity = async (activityId, title, date) => {
225+
let activityFound = null;
226+
227+
mockTrackerData.forEach((trackedInternship) => {
228+
const activity = trackedInternship.activity.find(
229+
(a) => a.id === activityId
230+
);
231+
if (activity) {
232+
activity.title = title;
233+
activity.date = date;
234+
activityFound = activity;
235+
}
236+
});
237+
238+
if (!activityFound)
239+
throw new Error(`No activity found with id: ${activityId}`);
240+
return activityFound;
241+
};
242+
243+
/**
244+
* Delete an activity by activity ID
245+
* @param activityId activity id
246+
*/
247+
export const deleteActivity = async (activityId) => {
248+
let deleted = false;
249+
250+
for (const tracked of mockTrackerData) {
251+
const index = tracked.activity.findIndex((act) => act.id === activityId);
252+
if (index !== -1) {
253+
tracked.activity.splice(index, 1);
254+
return;
255+
}
256+
}
257+
258+
if (!deleted) throw new Error(`No activity found with id: ${activityId}`);
259+
};
260+
163261
/**
164262
* Fetch user account information to be displayed on account settings page
165263
* @returns user

0 commit comments

Comments
 (0)