Skip to content

Commit 417ed1b

Browse files
authored
Merge pull request #4021 from Northeastern-Electric-Racing/#4003-guest-projects-page
#4003 guest projects page
2 parents 75c71be + 9673df3 commit 417ed1b

8 files changed

Lines changed: 229 additions & 4 deletions

File tree

src/backend/src/prisma-query-args/projects.query-args.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ export const getProjectPreviewQueryArgs = (organizationId: string) =>
100100
abbreviation: true,
101101
teams: {
102102
select: {
103+
teamType: {
104+
select: {
105+
teamTypeId: true,
106+
name: true
107+
}
108+
},
103109
teamId: true,
104110
teamName: true
105111
}
@@ -132,6 +138,12 @@ export const getProjectOverviewQueryArgs = (organizationId: string) =>
132138
abbreviation: true,
133139
teams: {
134140
select: {
141+
teamType: {
142+
select: {
143+
teamTypeId: true,
144+
name: true
145+
}
146+
},
135147
teamId: true,
136148
teamName: true
137149
}

src/backend/src/services/organizations.services.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ export default class OrganizationsService {
410410
throw new NotFoundException('Organization', organizationId);
411411
}
412412

413-
return organization.featuredProjects.map(projectPreviewTransformer);
413+
return organization.featuredProjects.filter((p) => !p.wbsElement.dateDeleted).map(projectPreviewTransformer);
414414
}
415415

416416
/**

src/backend/src/transformers/projects.transformer.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ export const projectPreviewTransformer = (project: Prisma.ProjectGetPayload<Proj
117117
duration: calculateDuration(project.workPackages),
118118
startDate: calculateProjectStartDate(project.workPackages),
119119
abbreviation: project.abbreviation ?? undefined,
120+
teamTypes: Array.from(
121+
project.teams
122+
.reduce((acc, team) => {
123+
if (team.teamType) {
124+
acc.set(team.teamType.teamTypeId, {
125+
name: team.teamType.name,
126+
teamTypeId: team.teamType.teamTypeId
127+
});
128+
}
129+
return acc;
130+
}, new Map<string, { name: string; teamTypeId: string }>())
131+
.values()
132+
),
120133
teams: project.teams,
121134
workPackages: project.workPackages.map((wp) => ({
122135
...wp,
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { alpha, Box, Card, CardContent, Chip, Stack, Typography, useTheme, Link } from '@mui/material';
2+
import { wbsNamePipe, ProjectPreview, wbsPipe, WbsElementStatus } from 'shared';
3+
import { datePipe } from '../../utils/pipes';
4+
import { NERButton } from '../../components/NERButton';
5+
import { useSingleProject } from '../../hooks/projects.hooks';
6+
import LoadingIndicator from '../../components/LoadingIndicator';
7+
import ErrorPage from '../ErrorPage';
8+
import { Link as RouterLink } from 'react-router-dom';
9+
10+
interface ProjectCardProps {
11+
project: ProjectPreview;
12+
}
13+
14+
const GuestProjectsCard: React.FC<ProjectCardProps> = ({ project }) => {
15+
const theme = useTheme();
16+
const { data: singleProject, isLoading, isError, error } = useSingleProject(project.wbsNum);
17+
if (isError) return <ErrorPage message={error.message} />;
18+
if (isLoading || !singleProject) return <LoadingIndicator />;
19+
20+
const activeWorkPackages = project.workPackages.filter((wp) => wp.status === WbsElementStatus.Active);
21+
22+
return (
23+
<Card
24+
variant="outlined"
25+
sx={{
26+
width: '100%',
27+
height: '100%',
28+
display: 'flex',
29+
flexDirection: 'column',
30+
background: theme.palette.background.paper,
31+
borderRadius: 2
32+
}}
33+
>
34+
<CardContent sx={{ padding: 2, display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
35+
<Stack direction="row" justifyContent="space-between">
36+
<Box width={'100%'}>
37+
<Box display="flex" justifyContent="space-between" alignItems="center">
38+
<Typography
39+
fontWeight={'regular'}
40+
variant="h5"
41+
sx={{ marginBottom: '0.3rem', fontSize: { xs: '1.15rem', sm: '1.5rem' }, flexGrow: 1 }}
42+
>
43+
{wbsNamePipe(singleProject)}
44+
</Typography>
45+
{activeWorkPackages[0]?.stage ? (
46+
<Chip
47+
size="medium"
48+
variant="filled"
49+
sx={{
50+
marginLeft: 'auto',
51+
fontSize: 12,
52+
bgcolor: alpha(theme.palette.primary.main, 0.45),
53+
color: theme.palette.primary.light
54+
}}
55+
label={activeWorkPackages[0].stage}
56+
/>
57+
) : null}
58+
</Box>
59+
<Typography fontSize={12} color="text.secondary">
60+
Project Lead:{' '}
61+
{singleProject.lead?.firstName && singleProject.lead?.lastName
62+
? `${singleProject.lead.firstName} ${singleProject.lead.lastName}`
63+
: 'N/A'}
64+
{' • '}
65+
Project Manager:{' '}
66+
{singleProject.manager?.firstName && singleProject.manager?.lastName
67+
? `${singleProject.manager.firstName} ${singleProject.manager.lastName}`
68+
: 'N/A'}
69+
</Typography>
70+
<Typography fontWeight={'regular'} fontSize={{ xs: 14, sm: 16 }}>
71+
{datePipe(singleProject.startDate) +
72+
' ⟝ ' +
73+
singleProject.duration +
74+
' wks ⟞ ' +
75+
datePipe(singleProject.endDate)}
76+
</Typography>
77+
</Box>
78+
</Stack>
79+
<Typography
80+
sx={{
81+
flexGrow: 1,
82+
overflow: 'hidden',
83+
textOverflow: 'ellipsis',
84+
display: '-webkit-box',
85+
WebkitLineClamp: 3,
86+
WebkitBoxOrient: 'vertical'
87+
}}
88+
>
89+
{singleProject.summary}
90+
</Typography>
91+
<Link
92+
component={RouterLink}
93+
to={`/projects/${wbsPipe(project.wbsNum)}`}
94+
sx={{ width: '100%', textDecoration: 'none' }}
95+
>
96+
<NERButton
97+
fullWidth
98+
sx={{
99+
marginTop: 2,
100+
backgroundColor: theme.palette.error.main,
101+
color: theme.palette.error.contrastText,
102+
'&:hover': {
103+
backgroundColor: theme.palette.error.dark
104+
}
105+
}}
106+
>
107+
Learn more
108+
</NERButton>
109+
</Link>
110+
</CardContent>
111+
</Card>
112+
);
113+
};
114+
115+
export default GuestProjectsCard;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useAllProjects } from '../../hooks/projects.hooks';
2+
import LoadingIndicator from '../../components/LoadingIndicator';
3+
import ErrorPage from '../ErrorPage';
4+
import { Box, useMediaQuery } from '@mui/system';
5+
import { wbsPipe } from 'shared';
6+
import PageLayout from '../../components/PageLayout';
7+
import GuestProjectsCard from './GuestProjectsCard';
8+
import { useAllTeamTypes } from '../../hooks/team-types.hooks';
9+
import { Chip } from '@mui/material';
10+
import { useState } from 'react';
11+
12+
const GuestProjectsPage: React.FC = () => {
13+
const { data: allProjects, isLoading, isError, error } = useAllProjects();
14+
const [selectedTeamTypes, setSelectedTeamTypes] = useState<string[]>([]);
15+
const isMobilePortrait = useMediaQuery('(max-width:480px)');
16+
const {
17+
isLoading: teamTypesIsLoading,
18+
isError: teamTypesIsError,
19+
data: teamTypes,
20+
error: teamTypesError
21+
} = useAllTeamTypes();
22+
23+
if (isLoading || !allProjects || teamTypesIsLoading || !teamTypes) return <LoadingIndicator />;
24+
if (isError) return <ErrorPage message={error.message} />;
25+
if (teamTypesIsError) return <ErrorPage message={teamTypesError.message} />;
26+
27+
const filteredProjects = allProjects.filter(
28+
(project) =>
29+
selectedTeamTypes.length === 0 || project.teamTypes.some((t) => t !== null && selectedTeamTypes.includes(t.name))
30+
);
31+
32+
return (
33+
<PageLayout title="Projects">
34+
<Box
35+
width={'100%'}
36+
display={'flex'}
37+
justifyContent={'center'}
38+
gap={2}
39+
mb={3}
40+
sx={{
41+
overflowX: 'auto',
42+
pb: 1
43+
}}
44+
>
45+
{teamTypes.map((team) => (
46+
<Chip
47+
key={team.name}
48+
label={team.name}
49+
onClick={() =>
50+
setSelectedTeamTypes((prev) =>
51+
prev?.includes(team.name) ? prev.filter((t: string) => t !== team.name) : [...(prev || []), team.name]
52+
)
53+
}
54+
clickable
55+
color={selectedTeamTypes?.includes(team.name) ? 'primary' : 'default'}
56+
sx={{ flexShrink: 0 }}
57+
/>
58+
))}
59+
</Box>
60+
<Box
61+
sx={{
62+
display: 'grid',
63+
gridTemplateColumns: isMobilePortrait ? '1fr' : 'repeat(3, 1fr)',
64+
gap: isMobilePortrait ? 2 : 3,
65+
width: '100%',
66+
px: isMobilePortrait ? 1 : 0
67+
}}
68+
>
69+
{filteredProjects.map((p) => (
70+
<GuestProjectsCard key={wbsPipe(p.wbsNum)} project={p} />
71+
))}
72+
</Box>
73+
</PageLayout>
74+
);
75+
};
76+
77+
export default GuestProjectsPage;

src/frontend/src/pages/HomePage/components/FeaturedProjects.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,14 @@
33
* See the LICENSE file in the repository root folder for details.
44
*/
55

6-
import FeaturedProjectsCard from './FeaturedProjectsCard';
76
import { useFeaturedProjects } from '../../../hooks/organizations.hooks';
87
import ErrorPage from '../../ErrorPage';
9-
import { wbsPipe } from 'shared';
108
import LoadingIndicator from '../../../components/LoadingIndicator';
119
import ScrollablePageBlock from './ScrollablePageBlock';
1210
import EmptyPageBlockDisplay from './EmptyPageBlockDisplay';
1311
import { Box, Stack, useMediaQuery } from '@mui/material';
1412
import { Error } from '@mui/icons-material';
13+
import GuestProjectsCard from '../../GuestProjectsPage/GuestProjectsCard';
1514

1615
const NoFeaturedProjectsDisplay: React.FC = () => {
1716
return (
@@ -51,7 +50,11 @@ const FeaturedProjects: React.FC = () => {
5150
{featuredProjects.length === 0 ? (
5251
<NoFeaturedProjectsDisplay />
5352
) : (
54-
featuredProjects.map((p) => <FeaturedProjectsCard key={wbsPipe(p.wbsNum)} project={p} />)
53+
featuredProjects.map((p) => (
54+
<Box key={p.wbsNum.projectNumber} sx={{ width: isMobilePortrait ? '100%' : 300, flexShrink: 0 }}>
55+
<GuestProjectsCard project={p} />
56+
</Box>
57+
))
5558
)}
5659
</Stack>
5760
</ScrollablePageBlock>

src/frontend/src/pages/ProjectsPage/ProjectsPage.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { useCurrentUser } from '../../hooks/users.hooks';
1414
import { isGuest } from 'shared';
1515
import { Add } from '@mui/icons-material';
1616
import { useHistory } from 'react-router-dom';
17+
import GuestProjectsPage from '../GuestProjectsPage/GuestProjectsPage';
1718

1819
/**
1920
* Cards of all projects that this user is in their team.
@@ -24,6 +25,9 @@ const ProjectsPage: React.FC = () => {
2425
const user = useCurrentUser();
2526
const history = useHistory();
2627

28+
if (isGuest(user.role)) {
29+
return <GuestProjectsPage />;
30+
}
2731
return (
2832
<PageLayout
2933
title="Projects"

src/shared/src/types/project-types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface ProjectPreview extends WbsElementPreview {
8282
abbreviation?: string;
8383
workPackages: WorkPackagePreview[];
8484
teams: { teamName: string; teamId: string }[];
85+
teamTypes: { name: string; teamTypeId: string }[];
8586
}
8687

8788
export interface ProjectOverview extends ProjectPreview {

0 commit comments

Comments
 (0)