Skip to content
Open
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
1 change: 1 addition & 0 deletions src/config/team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ITeam {
}

export interface IMemberName {
id: string;
firstName: string;
lastName: string;
school?: string;
Expand Down
11 changes: 9 additions & 2 deletions src/features/Search/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { getOptionsFromEnum } from '../../util';

interface IFilterProps {
initFilters: ISearchParameter[];
onChange: (newFilters: ISearchParameter[], reviewStatus?: number[], reviewScore?: number[]) => void;
onChange: (newFilters: ISearchParameter[], reviewStatus?: number[], reviewScore?: number[], groupTeams?: boolean) => void;
onResetForm: () => void;
loading: boolean;
}
Expand Down Expand Up @@ -65,6 +65,7 @@ class FilterComponent extends React.Component<IFilterProps, {}> {
),
reviewer1: this.searchParam2List('reviewerName', initFilters),
reviewer2: this.searchParam2List('reviewerName2', initFilters),
groupTeams: false,
};
return initVals;
}
Expand Down Expand Up @@ -181,6 +182,12 @@ class FilterComponent extends React.Component<IFilterProps, {}> {
options={getOptionsFromEnum(reviewers)}
value={fp.values.reviewer2}
/>
<FastField
name={'groupTeams'}
component={FormikElements.Checkbox}
label={'Show only hackers with teams (grouped by team)'}
value={fp.values.groupTeams}
/>
<Flex justifyContent={'center'}>
<Box mr={'10px'}>
<Button
Expand Down Expand Up @@ -259,7 +266,7 @@ class FilterComponent extends React.Component<IFilterProps, {}> {
);
// this.props.onChange(search);
// this.props.onChange(search, values.reviewStatus);
this.props.onChange(search, values.reviewStatus, values.reviewScore);
this.props.onChange(search, values.reviewStatus, values.reviewScore, values.groupTeams || false);
}

/**
Expand Down
39 changes: 36 additions & 3 deletions src/features/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ interface ISearchState {
sponsor?: ISponsor;
reviewStatusFilter: number[];
reviewScoreFilter: number[];
groupTeamsFilter: boolean;
emailModalOpen: boolean;
reviewerModalOpen: boolean;
emailSending: boolean;
Expand Down Expand Up @@ -81,6 +82,7 @@ class SearchContainer extends React.Component<{}, ISearchState> {
viewSaved: false,
reviewStatusFilter: [],
reviewScoreFilter: [],
groupTeamsFilter: false,
emailModalOpen: false,
reviewerModalOpen: false,
emailSending: false,
Expand Down Expand Up @@ -767,11 +769,12 @@ class SearchContainer extends React.Component<{}, ISearchState> {
this.updateQueryURL([], this.state.searchBar);
}

private onFilterChange(newFilters: ISearchParameter[], reviewStatus: number[], reviewScore: number[]) {
private onFilterChange(newFilters: ISearchParameter[], reviewStatus: number[], reviewScore: number[], groupTeams: boolean = false) {
this.setState({
query: newFilters,
reviewStatusFilter: reviewStatus || [],
reviewScoreFilter: reviewScore || [],
groupTeamsFilter: groupTeams,
}, () => {
this.updateQueryURL(newFilters, this.state.searchBar);
this.triggerSearch();
Expand Down Expand Up @@ -856,10 +859,21 @@ class SearchContainer extends React.Component<{}, ISearchState> {
}
}

// extract teamId from hacker (handles both ObjectId and string)
private getTeamId(hacker: IHacker): string | null {
if (!hacker.teamId) {
return null;
}
if (typeof hacker.teamId === 'object' && hacker.teamId !== null) {
return String((hacker.teamId as any)._id || (hacker.teamId as any).id);
}
return String(hacker.teamId);
}

private filter() {
const { sponsor, viewSaved, results } = this.state;
const searchBar = this.state.searchBar.toLowerCase();
return results.filter(({ hacker }) => {
let filteredResults = results.filter(({ hacker }) => {
const { accountId } = hacker;
let foundAcct;
if (typeof accountId !== 'string') {
Expand Down Expand Up @@ -903,8 +917,27 @@ class SearchContainer extends React.Component<{}, ISearchState> {
!viewSaved ||
(sponsor && sponsor.nominees.some((n) => n === hacker.id));

return (foundAcct || foundHacker) && isSavedBySponsorIfToggled && passReviewStatusFilter && passReviewScoreFilter;
// exclude hackers without teams if groupTeamsFilter is enabled
const passTeamFilter = !this.state.groupTeamsFilter || this.getTeamId(hacker) !== null;

return (foundAcct || foundHacker) && isSavedBySponsorIfToggled && passReviewStatusFilter && passReviewScoreFilter && passTeamFilter;
});

// sort results to group teammates together if groupTeamsFilter is enabled
if (this.state.groupTeamsFilter) {
filteredResults.sort((a, b) => {
const teamIdA = this.getTeamId(a.hacker);
const teamIdB = this.getTeamId(b.hacker);

// group by teamId (same teamId means they're on the same team)
if (teamIdA === teamIdB) return 0;

// sort by teamId to group teammates together
return teamIdA!.localeCompare(teamIdB!);
});
}

return filteredResults;
}

private toggleSaved = async () => {
Expand Down
108 changes: 108 additions & 0 deletions src/features/SingleHacker/SingleHackerView.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import React, { useEffect, useState } from 'react';
import Helmet from 'react-helmet';
import { Input } from '../../shared/Form';
import { Link } from 'react-router-dom';

import { Box, Flex } from '@rebass/grid';
import { toast } from 'react-toastify';

import { Hacker } from '../../api';
import Team from '../../api/team';
import {
FrontendRoute,
HACKATHON_NAME,
HackerStatus,
HackerReviewerStatus,
IAccount,
IHacker,
IMemberName,
UserType,
} from '../../config';
import { ITeamResponse } from '../../config/teamGETResponse';
import {
Button,
ButtonVariant,
Expand Down Expand Up @@ -50,6 +55,8 @@ const SingleHackerView: React.FC<IHackerViewProps> = (props) => {
const [reviewerComments2, setReviewerComments2] = useState(props.hacker.reviewerComments2);
const [isAdmin, setIsAdmin] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [teamMembers, setTeamMembers] = useState<IMemberName[]>([]);
const [isLoadingTeam, setIsLoadingTeam] = useState(false);

useEffect(() => {
setStatus(props.hacker.status);
Expand All @@ -66,6 +73,49 @@ const SingleHackerView: React.FC<IHackerViewProps> = (props) => {
setReviewerStatus2(props.hacker.reviewerStatus2);
}, [props]);

// Fetch team members
useEffect(() => {
const fetchTeamMembers = async () => {
// only if hacker has a teamId
if (props.hacker.teamId) {
setIsLoadingTeam(true);
// teamId might be an object (populated) or a string/ObjectId
// extract the ID if it's an object, otherwise use it as-is
let teamId: string;
if (typeof props.hacker.teamId === 'object' && props.hacker.teamId !== null) {
teamId = String((props.hacker.teamId as any)._id || (props.hacker.teamId as any).id);
} else {
teamId = String(props.hacker.teamId);
}
try {
const teamResponse: ITeamResponse = (await Team.get(teamId)).data.data;

// filter out the current hacker from the team members list
// convert both IDs to strings for comparison to handle ObjectId vs string mismatches
const currentHackerId = String(props.hacker.id);
const otherMembers = teamResponse.members.filter(
(member) =>
member &&
String(member.id) !== currentHackerId &&
member.id &&
member.firstName &&
member.lastName
);

setTeamMembers(otherMembers);
} catch (e: any) {
setTeamMembers([]);
} finally {
setIsLoadingTeam(false);
}
} else {
setTeamMembers([]);
}
};

fetchTeamMembers();
}, [props.hacker.teamId, props.hacker.id]);

const submit = async () => {
if (!isStaffMember && !isHackboardMember) {
return;
Expand Down Expand Up @@ -389,6 +439,64 @@ const SingleHackerView: React.FC<IHackerViewProps> = (props) => {
link={hackerDetails.application.general.URL.dribbble}
/>
</Flex>
<hr />
{/* Team Members Section */}
{props.hacker.teamId && (
<>
<H2 color={theme.colors.black60}>Team Members</H2>
{isLoadingTeam ? (
<Box>Loading team members...</Box>
) : teamMembers.length > 0 ? (
<Flex
width="100%"
flexWrap="wrap"
flexDirection="column"
style={{ marginTop: '1em' }}
>
{teamMembers.map((member: IMemberName) => {
const hackerPage = FrontendRoute.VIEW_HACKER_PAGE.replace(
':id',
member.id
);
return (
<Box key={member.id} mb="10px">
<Link to={hackerPage} style={{ textDecoration: 'none' }}>
<Box
style={{
padding: '8px 12px',
border: `1px solid ${theme.colors.purpleLight}`,
borderRadius: '4px',
cursor: 'pointer',
transition: 'background-color 0.2s',
}}
onMouseEnter={(e: any) => {
e.currentTarget.style.backgroundColor =
theme.colors.purpleLight;
}}
onMouseLeave={(e: any) => {
e.currentTarget.style.backgroundColor = 'transparent';
}}
>
<strong>
{member.firstName} {member.lastName}
</strong>
{member.school && (
<Box style={{ fontSize: '14px', color: theme.colors.black60 }}>
{member.school}
</Box>
)}
</Box>
</Link>
</Box>
);
})}
</Flex>
) : (
<Box>No other team members found.</Box>
)}
<hr />
</>
)}
{/* Only tier1 sponsors and admin have access to user resumes */}
{props.userType === UserType.SPONSOR_T1 || canViewAdminSection ? (
<Flex flexDirection={'column'} style={{ marginTop: '4em' }}>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Application/View/[id].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const SingleHackerPage: React.FC = () => {
(async () => {
try {
const viewer = (await Account.getSelf()).data.data;
console.log(viewer, viewer.accountType);
// console.log(viewer, viewer.accountType);
setUserType(viewer.accountType);
const newHacker = (await Hacker.get(id)).data.data;
const account = (await Account.get(newHacker.accountId as string))
Expand Down