Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/frontendtests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
working-directory: client
strategy:
matrix:
node-version: [14.x, 16.x, 18.x]
node-version: [18.x, 20.x]
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the node version of frontendtests Github Action since I'm using "@mui/lab" and "@mui/x-date-pickers", which require at least node v16. Although they use the same MUI version (v5), they have stricter deps than "@mui/materal", so the node version used to run the tests needs to be updated

# See supported Node.js release schedule at https://nodejs.org/en/about/releases/

steps:
Expand Down
36,884 changes: 12,248 additions & 24,636 deletions client/package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
"@emotion/styled": "^11.10.6",
"@fontsource/poppins": "^4.5.10",
"@mui/icons-material": "^5.11.0",
"@mui/lab": "^5.0.0-alpha.169",
"@mui/material": "^5.11.16",
"@mui/x-date-pickers": "^5.0.20",
"axios": "^1.3.4",
"date-fns": "^2.29.3",
"dayjs": "^1.11.10",
Expand Down
110 changes: 110 additions & 0 deletions client/src/components/TrackedActivity.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import AddRoundedIcon from '@mui/icons-material/AddRounded';
import {
Timeline,
TimelineConnector,
TimelineContent,
TimelineDot,
TimelineItem,
TimelineOppositeContent,
TimelineSeparator,
} from '@mui/lab';
import { Button } from '@mui/material';
import React, { useEffect, useState } from 'react';

import { getActivitiesByTrackedInternshipId } from '../utils/api';
import { addActivity } from '../utils/api';
import TrackedActivityRow from './TrackedActivityRow';

const TrackedActivity = ({ trackedInternshipId }) => {
const [activity, setActivity] = useState([]);
const [isActivityUpdated, setIsActivityUpdated] = useState(true);

const addEvent = async () => {
try {
// By setting the title to an empty string, the added event will default to edit mode
await addActivity(trackedInternshipId, '', null);
setIsActivityUpdated(true);
} catch (err) {
console.error('Error adding activity:', err);
}
};

useEffect(() => {
const fetchActivities = async () => {
if (trackedInternshipId) {
try {
const allActivity = await getActivitiesByTrackedInternshipId(
trackedInternshipId
);
setActivity(allActivity);
} catch (error) {
console.error('Error getting internship activity.', error);
}
}
};

if (isActivityUpdated) {
fetchActivities();
setIsActivityUpdated(false);
}
}, [trackedInternshipId, isActivityUpdated]);

return (
<>
{activity.length > 0 && (
<Timeline sx={{ p: 0, m: 0 }}>
{activity.map((event, index) => (
<TimelineItem sx={{ minHeight: '3.2rem' }} key={event.id}>
<TimelineOppositeContent
sx={{ flex: 0 }}
></TimelineOppositeContent>
<TimelineSeparator>
<TimelineDot
variant="outlined"
sx={{
borderColor: 'tertiary.main',
backgroundColor:
event.date == null || new Date(event.date) > new Date()
? 'white'
: 'background.dark',
width: '1.1rem',
height: '1.1rem',
marginY: '.5rem',
}}
/>
{index < activity.length - 1 && (
<TimelineConnector
sx={{
borderColor: 'background.dark',
borderStyle: 'dashed',
backgroundColor: 'white',
borderWidth: 1,
}}
/>
)}
</TimelineSeparator>
<TimelineContent sx={{ m: 0, p: 0 }}>
<TrackedActivityRow
event={event}
onEventUpdated={() => setIsActivityUpdated(true)}
defaultEditMode={!event.title}
/>
</TimelineContent>
</TimelineItem>
))}
</Timeline>
)}
<Button
variant="rounded"
color="primary"
onClick={addEvent}
startIcon={<AddRoundedIcon />}
sx={{ mb: '1rem', mt: '.5rem' }}
>
Add task
</Button>
</>
);
};

export default TrackedActivity;
121 changes: 121 additions & 0 deletions client/src/components/TrackedActivityRow.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import CheckCircleRoundedIcon from '@mui/icons-material/CheckCircleRounded';
import DeleteRoundedIcon from '@mui/icons-material/DeleteRounded';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import { IconButton, Stack, TextField, Typography } from '@mui/material';
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import React, { useState } from 'react';

import { deleteActivity, editActivity } from '../utils/api';

const TEXT_FIELD_STYLE = {
'& .MuiInputBase-input': {
fontSize: '0.9rem',
fontWeight: 200,
},
'& .MuiInputLabel-root': {
fontSize: '0.9rem !important',
fontWeight: 200,
},
'& .MuiInputLabel-shrink': {
fontWeight: 200,
},
};

const TrackedActivityRow = ({
event,
onEventUpdated,
defaultEditMode = false,
}) => {
const [isEditMode, setIsEditMode] = useState(defaultEditMode);
const [title, setTitle] = useState(event.title);
const [date, setDate] = useState(event.date);

const updateEvent = async () => {
try {
await editActivity(event.id, title, date);
setIsEditMode(false);
} catch (err) {
console.error('Error updating activity:', err);
}
onEventUpdated();
};

const deleteEvent = async () => {
try {
await deleteActivity(event.id);
} catch (err) {
console.error('Error deleting activity:', err);
}
onEventUpdated();
};

return (
<LocalizationProvider dateAdapter={AdapterDateFns}>
<Stack
direction="row"
justifyContent="space-between"
alignItems="center"
paddingBottom="1rem"
paddingLeft="1rem"
borderRadius="1rem"
spacing={4}
>
{isEditMode ? (
<TextField
label="Activity"
size="small"
fullWidth
value={title}
onChange={(event) => setTitle(event.target.value)}
sx={TEXT_FIELD_STYLE}
/>
) : (
<Typography variant="body3" fontWeight={200}>
{event.title}
</Typography>
)}
<Stack direction="row" alignItems="center" spacing={2} py={0}>
{isEditMode ? (
<DatePicker
label="Date"
value={date}
onChange={(newDate) => setDate(newDate)}
renderInput={(params) => (
<TextField
{...params}
size="small"
sx={{
...TEXT_FIELD_STYLE,
minWidth: '9.5rem',
}}
/>
)}
/>
) : (
<Typography variant="body3" color="text.light" fontWeight={200}>
{event.date ? new Date(event.date).toLocaleDateString() : ''}
</Typography>
)}
<Stack direction="row" py={0}>
{isEditMode ? (
<IconButton onClick={updateEvent}>
<CheckCircleRoundedIcon fontSize="small" />
</IconButton>
) : (
<IconButton onClick={() => setIsEditMode(true)}>
<EditRoundedIcon fontSize="small" />
</IconButton>
)}
<IconButton onClick={deleteEvent}>
<DeleteRoundedIcon fontSize="small" />
</IconButton>
</Stack>
</Stack>
</Stack>
</LocalizationProvider>
);
};

export default TrackedActivityRow;
9 changes: 9 additions & 0 deletions client/src/components/TrackerDrawer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import InternshipTag from './InternshipTag';
import JobDescription from './JobDescription';
import Loading from './Loading';
import StatusDropdown from './StatusDropdown';
import TrackedActivity from './TrackedActivity';

const TrackerDrawer = ({
trackedInternshipId,
Expand Down Expand Up @@ -172,6 +173,14 @@ const TrackerDrawer = ({
requirements={internshipInfo.jobInfo.jobReqs}
responsibilities={internshipInfo.jobInfo.jobResp}
/>
<Typography
variant="h6"
paddingTop="1.5rem"
paddingBottom="1rem"
>
Activity & Tasks
</Typography>
<TrackedActivity trackedInternshipId={trackedInternshipId} />
</Box>
</Box>
<Box bgcolor="background.main" padding="1rem 0.8rem 1rem 1.2rem">
Expand Down
98 changes: 98 additions & 0 deletions client/src/utils/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,104 @@ export const addNote = async (trackedInternshipId, message) => {
return newNote;
};

/**
* Get all activities from a tracked internship
* @param trackedInternshipId tracked internship id
* @returns an array of activities
*/
export const getActivitiesByTrackedInternshipId = async (
trackedInternshipId
) => {
const trackedInternship = mockTrackerData.find(
(trackedInternship) => trackedInternship.id === trackedInternshipId
);

if (!trackedInternship)
throw new Error(
`No tracked internship associated with id: ${trackedInternshipId}.`
);

// Sort activity in chronological order
const sortedActivity = [...trackedInternship.activity].sort((a, b) => {
const timeA = a.date ? new Date(a.date).getTime() : Infinity;
const timeB = b.date ? new Date(b.date).getTime() : Infinity;
return timeA - timeB;
});

return sortedActivity;
};

/**
* Add an activity to a tracked internship
* @param trackedInternshipId tracked internship id
* @param title activity title
* @param date activity date (can be null)
* @returns newly created activity
*/
export const addActivity = async (trackedInternshipId, title, date = null) => {
const trackedInternship = mockTrackerData.find(
(ti) => ti.id === trackedInternshipId
);

if (!trackedInternship)
throw new Error(
`No tracked internship found with id: ${trackedInternshipId}`
);

const newActivityId =
trackedInternship.activity.length > 0
? Math.max(...trackedInternship.activity.map((a) => a.id)) + 1
: 0;

const newActivity = { id: newActivityId, title, date };
trackedInternship.activity.push(newActivity);
return newActivity;
};

/**
* Edit an activity by activity ID
* @param activityId activity id
* @param title new title
* @param date new date
* @returns updated activity
*/
export const editActivity = async (activityId, title, date) => {
let activityFound = null;

mockTrackerData.forEach((trackedInternship) => {
const activity = trackedInternship.activity.find(
(a) => a.id === activityId
);
if (activity) {
activity.title = title;
activity.date = date;
activityFound = activity;
}
});

if (!activityFound)
throw new Error(`No activity found with id: ${activityId}`);
return activityFound;
};

/**
* Delete an activity by activity ID
* @param activityId activity id
*/
export const deleteActivity = async (activityId) => {
let deleted = false;

for (const tracked of mockTrackerData) {
const index = tracked.activity.findIndex((act) => act.id === activityId);
if (index !== -1) {
tracked.activity.splice(index, 1);
return;
}
}

if (!deleted) throw new Error(`No activity found with id: ${activityId}`);
};

/**
* Fetch user account information to be displayed on account settings page
* @returns user
Expand Down
Loading