diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 46b703952..0365ba164 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0](https://github.com/hackmcgill/dashboard/tree/2.3.0) - 2020-01-15 + +### Added + +- Status page displays appropriate message based on hacker status +- Job interest column for sponsors to see more info on hacker +- Added buttons for status page based on status +- Added travel page that displays hacker's travel status and reimbursement amount + +### Changed + +- Update application confirm deadline +- Update travel page text +- Update global styles for `a` tag + +### Fixed + +- Fixed search page not loading properly +- Fixed search queries not working +- Update saved hackers for sponsors to view without refreshing page +- Fixed search page not loading properly +- Fix withdrawn button on status page + ## [2.2.2](https://github.com/hackmcgill/dashboard/tree/2.2.2) - 2020-01-05 ### Changed diff --git a/package-lock.json b/package-lock.json index 4f211a52b..e1762a337 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "hackerapi-frontend", - "version": "2.2.2", + "version": "2.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index e5e692dbc..9aaf24fc5 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "hackerapi-frontend", "homepage": "https://app.mchacks.ca", - "version": "2.2.2", + "version": "2.3.0", "private": true, "dependencies": { "@rebass/grid": "^6.0.0-7", diff --git a/src/App.tsx b/src/App.tsx index 251531130..a83ebda2f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -20,6 +20,7 @@ import SingleHackerContainer from './features/SingleHacker/Main'; import CreateSponsorContainer from './features/Sponsor/SponsorCreation'; import EditSponsorContainer from './features/Sponsor/SponsorEdition'; import TeamContainer from './features/Team/Main'; +import TravelContainer from './features/Travel/Main'; import { FrontendRoute, @@ -41,6 +42,7 @@ import { canAccessApplication, canAccessHackerPass, canAccessTeam, + canAccessTravel, isSponsor, userCanAccessHackerPage, } from './util'; @@ -168,6 +170,23 @@ class App extends React.Component { ), { activePage: 'team' } )} /> + + user.confirmed && user.accountType === UserType.HACKER, + } + ), { activePage: 'travel' } + )} + /> >> { + const cached: any = LocalCache.get(CACHE_TRAVEL_KEY); + if (cached && !overrideCache) { + return cached as AxiosResponse>; + } + const value = await API.getEndpoint(APIRoute.TRAVEL_SELF).getAll(); + LocalCache.set(CACHE_TRAVEL_KEY, value); + return value; + } + /** + * Get information about a travel + * @param id the ID of the travel + */ + public async get( + id: string, + overrideCache?: boolean + ): Promise>> { + const key = CACHE_TRAVEL_KEY + '-' + id; + const cached: any = LocalCache.get(key); + if (cached && !overrideCache) { + return cached as Promise>>; + } + const value = await API.getEndpoint(APIRoute.TRAVEL).getOne({ id }); + LocalCache.set(key, value); + return value; + } + + /** + * Get information about a travel + * @param id the ID of the travel + */ + public async getByEmail( + email: string, + overrideCache?: boolean + ): Promise>> { + const value = await API.getEndpoint(APIRoute.TRAVEL_EMAIL).getOne({ + id: email, + }); + return value; + } +} + +export const Travel = new TravelAPI(); + +export default Travel; diff --git a/src/assets/images/train.svg b/src/assets/images/train.svg new file mode 100644 index 000000000..3cc29b434 --- /dev/null +++ b/src/assets/images/train.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/config/APIRoute.ts b/src/config/APIRoute.ts index 06907136d..58ac7d833 100644 --- a/src/config/APIRoute.ts +++ b/src/config/APIRoute.ts @@ -21,6 +21,10 @@ export enum APIRoute { HACKER_SELF = 'hacker/self', HACKER_STATS = 'hacker/stats', HACKER_STATUS = 'hacker/status', + // Travel routes + TRAVEL = 'travel', + TRAVEL_EMAIL = 'travel/email', + TRAVEL_SELF = 'travel/self', // Search routes SEARCH = 'search', // Sponsor routes diff --git a/src/config/constants.ts b/src/config/constants.ts index 7e6b5bd9a..6dec8ee38 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -10,6 +10,7 @@ export const CACHE_USER_KEY = 'userInfo'; export const CACHE_HACKER_KEY = 'hackerInfo'; export const CACHE_STATS_KEY = 'statsInfo'; export const CACHE_SPONSOR_KEY = 'sponsorInfo'; +export const CACHE_TRAVEL_KEY = 'travelInfo'; // General information export const HACKATHON_NAME = 'McHacks'; @@ -52,7 +53,7 @@ export const NONE_STATUS_TEXT = export const APPLIED_STATUS_TEXT = 'Your application has been submitted. Decisions will be sent out in January so stay tuned!'; export const ACCEPTED_STATUS_TEXT = - "Congratulations! We're excited to offer you a spot at McHacks! Please RSVP by January 22, 2020 to secure your spot and we'll see you there."; + "Congratulations! We're excited to offer you a spot at McHacks! Please RSVP by January 20, 2020 at 11:59PM EST to secure your spot and we'll see you there."; export const DECLINED_STATUS_TEXT = "Thank you so much for your interest in McHacks. Unfortunately, we don't have enough space to offer you a spot this year. That being said, please keep in touch and we'd love to see you apply again next year! In the meantime, we hope you continue to create and build awesome things!"; export const WAITLISTED_STATUS_TEXT = @@ -60,7 +61,7 @@ export const WAITLISTED_STATUS_TEXT = export const CONFIRMED_STATUS_TEXT = 'Your attendance has been confirmed! More information on McHacks will be sent to your inbox as we get closer to the event.'; export const WITHDRAWN_STATUS_TEXT = - "We're sorry to hear you're unable to make it to McHacks this year. Please keep in touch and hopefully we'll see you at the next one"; + "We're sorry to hear you're unable to make it to McHacks this year. Please keep in touch and hopefully we'll see you at the next one."; export const CHECKED_IN_STATUS_TEXT = 'You’re checked-in and ready to go!'; // Application management diff --git a/src/config/frontendRoutes.ts b/src/config/frontendRoutes.ts index ee5acbdcf..af68d677d 100644 --- a/src/config/frontendRoutes.ts +++ b/src/config/frontendRoutes.ts @@ -15,6 +15,7 @@ export enum FrontendRoute { RESET_PASSWORD_PAGE = '/password/reset', SPONSOR_SEARCH_PAGE = '/sponsor/search', TEAM_PAGE = '/team', + TRAVEL_PAGE = '/travel', CREATE_SPONSOR_PAGE = '/sponsor/create', EDIT_SPONSOR_PAGE = '/sponsor/edit', diff --git a/src/config/index.ts b/src/config/index.ts index d540614d2..090b0ae7b 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -20,6 +20,8 @@ export * from './shirtSizes'; export * from './skills'; export * from './statsResponse'; export * from './team'; +export * from './travelStatus'; +export * from './travel'; export * from './userTypes'; export * from './validationError'; export * from './pageType'; diff --git a/src/config/travel.ts b/src/config/travel.ts new file mode 100644 index 000000000..b3373aa7c --- /dev/null +++ b/src/config/travel.ts @@ -0,0 +1,14 @@ +export interface ITravel { + // The travel's id + id: string; + // The id of the user that is travelling + accountId: string; + // The id of the hacker that is travlling + hackerId: string; + // The status of the hacker + status: string; + // The amount of money the traveller is requesting + request: number; + // The amount of money we are offering the traveller + offer: number; +} diff --git a/src/config/travelStatus.ts b/src/config/travelStatus.ts new file mode 100644 index 000000000..062988c82 --- /dev/null +++ b/src/config/travelStatus.ts @@ -0,0 +1,9 @@ +export enum TravelStatus { + TRAVEL_STATUS_NONE = 'None', + TRAVEL_STATUS_BUS = 'Bus', + TRAVEL_STATUS_OFFERED = 'Offered', + TRAVEL_STATUS_VALID = 'Valid', + TRAVEL_STATUS_INVALID = 'Invalid', + TRAVEL_STATUS_CLAIMED = 'Claimed', +} +export default TravelStatus; diff --git a/src/features/Nav/Navbar.tsx b/src/features/Nav/Navbar.tsx index b28e70137..e5e0c31b4 100644 --- a/src/features/Nav/Navbar.tsx +++ b/src/features/Nav/Navbar.tsx @@ -7,7 +7,7 @@ import { Hacker } from '../../api'; import Martlet from '../../assets/images/mchacks-martlet-tight.svg'; import { FrontendRoute as routes, HackerStatus } from '../../config'; // import { Image } from '../../shared/Elements'; -import { isLoggedIn } from '../../util/UserInfoHelperFunctions'; +import { isLoggedIn, canAccessTravel } from '../../util/UserInfoHelperFunctions'; import { isConfirmed } from '../../util/UserInfoHelperFunctions'; import Burger from './Burger'; import Icon from './Icon'; @@ -27,12 +27,13 @@ interface INavbarState { status: HackerStatus; confirmed: boolean; loaded: boolean; + showTravelLink: boolean; } export default class Navbar extends React.Component< INavbarProps, INavbarState -> { + > { constructor(props: INavbarProps) { super(props); this.state = { @@ -40,6 +41,7 @@ export default class Navbar extends React.Component< status: HackerStatus.HACKER_STATUS_NONE, confirmed: true, loaded: false, + showTravelLink: false }; this.checkLoggedIn(); } @@ -51,10 +53,10 @@ export default class Navbar extends React.Component< try { const response = await Hacker.getSelf(); hacker = response.data.data; - this.setState({ status: hacker.status }); + this.setState({ status: hacker.status, showTravelLink: canAccessTravel(hacker) }); } catch (e) { if (e.status === 401) { - this.setState({ status: HackerStatus.HACKER_STATUS_NONE }); + this.setState({ status: HackerStatus.HACKER_STATUS_NONE, showTravelLink: false }); } } @@ -80,7 +82,7 @@ export default class Navbar extends React.Component< appRoute = routes.EDIT_APPLICATION_PAGE; } - const route: any[] = [routes.HOME_PAGE, routes.EDIT_ACCOUNT_PAGE, appRoute]; + const route: any[] = [routes.HOME_PAGE, routes.EDIT_ACCOUNT_PAGE, appRoute, routes.TRAVEL_PAGE]; let NavItems = () => <>; if (loggedIn === true) { @@ -99,14 +101,24 @@ export default class Navbar extends React.Component< Profile {Date.now() < CONSTANTS.APPLICATION_CLOSE_TIME || - status !== HackerStatus.HACKER_STATUS_NONE ? ( + status !== HackerStatus.HACKER_STATUS_NONE ? ( + + Application + + ) : null} + {this.state.showTravelLink ? ( - Application + Travel ) : null} diff --git a/src/features/Search/Filters.tsx b/src/features/Search/Filters.tsx index 6fc86b565..55ebec18b 100644 --- a/src/features/Search/Filters.tsx +++ b/src/features/Search/Filters.tsx @@ -45,11 +45,17 @@ class FilterComponent extends React.Component { } private parseInitialValues(initFilters: ISearchParameter[]) { const initVals = { - school: this.searchParam2List('school', initFilters), - gradYear: this.searchParam2List('graduationYear', initFilters), - degree: this.searchParam2List('degree', initFilters), + school: this.searchParam2List('application.general.school', initFilters), + gradYear: this.searchParam2List( + 'application.general.graduationYear', + initFilters + ), + degree: this.searchParam2List('application.general.degree', initFilters), status: this.searchParam2List('status', initFilters), - skills: this.searchParam2List('application.skills', initFilters), + skills: this.searchParam2List( + 'application.shortAnswer.skills', + initFilters + ), jobInterest: this.searchParam2List( 'application.general.jobInterest', initFilters @@ -154,19 +160,25 @@ class FilterComponent extends React.Component { * @param values Formik values */ private handleSubmit(values: FormikValues) { - const schoolSearchParam = this.list2SearchParam('school', values.school); + const schoolSearchParam = this.list2SearchParam( + 'application.general.school', + values.school + ); const gradYearParam = this.list2SearchParam( - 'graduationYear', + 'application.general.graduationYear', values.gradYear ); - const degreeParam = this.list2SearchParam('degree', values.degree); + const degreeParam = this.list2SearchParam( + 'application.general.degree', + values.degree + ); const statusParam = this.list2SearchParam('status', values.status); const skillsParam = this.list2SearchParam( - 'application.skills', + 'application.shortAnswer.skills', values.skills ); const jobInterestParam = this.list2SearchParam( - 'application.jobInterest', + 'application.general.jobInterest', values.jobInterest ); let search: ISearchParameter[] = []; diff --git a/src/features/Search/ResultsTable.tsx b/src/features/Search/ResultsTable.tsx index 371816273..96519ab0c 100644 --- a/src/features/Search/ResultsTable.tsx +++ b/src/features/Search/ResultsTable.tsx @@ -22,8 +22,7 @@ const ResultsTable: React.StatelessComponent = (props) => { accessor: 'hacker.accountId.firstName', }, ]; - - const adminColumns = [ + const generalColumns = [ ...volunteerColumns, { Header: 'Last Name', @@ -31,20 +30,28 @@ const ResultsTable: React.StatelessComponent = (props) => { }, { Header: 'School', - accessor: 'hacker.school', + accessor: 'hacker.application.general.school', }, { - Header: 'Major', - accessor: 'hacker.major', + Header: 'Field of Study', + accessor: 'hacker.application.general.fieldOfStudy', }, { Header: 'Grad Year', - accessor: 'hacker.graduationYear', + accessor: 'hacker.application.general.graduationYear', }, + ]; + + const adminColumns = [ + ...generalColumns, { Header: 'Status', accessor: 'hacker.status', }, + { + Header: 'Job Interest', + accessor: 'hacker.application.general.jobInterest', + }, { Header: 'Applicant Info', Cell: ({ original }: any) => ( @@ -60,7 +67,23 @@ const ResultsTable: React.StatelessComponent = (props) => { ]; const sponsorColumns = [ - ...adminColumns, + ...generalColumns, + { + Header: 'Job Interest', + accessor: 'hacker.application.general.jobInterest', + }, + { + Header: 'Applicant Info', + Cell: ({ original }: any) => ( +
+ r.hacker)} + userType={props.userType} + /> +
+ ), + }, { Header: 'Save', Cell: ({ original }: any) => ( diff --git a/src/features/Search/Search.tsx b/src/features/Search/Search.tsx index a3a8fbe5f..5741bfaa4 100644 --- a/src/features/Search/Search.tsx +++ b/src/features/Search/Search.tsx @@ -142,10 +142,7 @@ class SearchContainer extends React.Component<{}, ISearchState> { const sponsor = (await Sponsor.getSelf()).data.data; this.setState({ sponsor }); } - - if (this.state.query.length > 0 || this.state.searchBar.length > 0) { - this.triggerSearch(); - } + await this.triggerSearch(); } private getSearchFromQuery(): ISearchParameter[] { const search = getValueFromQuery('q'); @@ -260,31 +257,40 @@ class SearchContainer extends React.Component<{}, ISearchState> { } private filter() { - const { sponsor, viewSaved, results, searchBar } = this.state; - + const { sponsor, viewSaved, results } = this.state; + const searchBar = this.state.searchBar.toLowerCase(); return results.filter(({ hacker }) => { const { accountId } = hacker; let foundAcct; if (typeof accountId !== 'string') { const account = accountId as IAccount; - const fullName = `${account.firstName} ${account.lastName}`; + const fullName = `${account.firstName} ${ + account.lastName + }`.toLowerCase(); foundAcct = fullName.includes(searchBar) || - account.email.includes(searchBar) || + account.email.toLowerCase().includes(searchBar) || account.phoneNumber.toString().includes(searchBar) || - // Removed as shirt size is no longer a properity of account: account.shirtSize.includes(searchBar) || - account.gender.includes(searchBar) || + account.gender.toLowerCase().includes(searchBar) || (account._id && account._id.includes(searchBar)); } else { foundAcct = accountId.includes(searchBar); } - const foundHacker = hacker.id.includes(searchBar) || - hacker.fieldOfStudy.includes(searchBar) || - hacker.school.includes(searchBar) || + hacker.application.general.school.includes(searchBar) || + hacker.application.general.degree.includes(searchBar) || + hacker.application.general.fieldOfStudy.includes(searchBar) || + hacker.application.general.graduationYear + .toString() + .includes(searchBar) || + hacker.application.general.jobInterest.includes(searchBar) || hacker.status.includes(searchBar) || - hacker.graduationYear.toString().includes(searchBar); + hacker.application.shortAnswer.question1.includes(searchBar) || + hacker.application.shortAnswer.question2.includes(searchBar) || + hacker.application.accommodation.shirtSize.includes(searchBar) || + (hacker.application.shortAnswer.skills && + hacker.application.shortAnswer.skills.toString().includes(searchBar)); const isSavedBySponsorIfToggled = !viewSaved || @@ -294,10 +300,12 @@ class SearchContainer extends React.Component<{}, ISearchState> { }); } - private toggleSaved = () => { - const { sponsor, viewSaved } = this.state; + private toggleSaved = async () => { + // Resets the sponsor if they made changes to their saved hackers + const sponsor = (await Sponsor.getSelf()).data.data; + const { viewSaved } = this.state; if (sponsor) { - this.setState({ viewSaved: !viewSaved }); + this.setState({ sponsor, viewSaved: !viewSaved }); } }; } diff --git a/src/features/Status/StatusPage.tsx b/src/features/Status/StatusPage.tsx index 0ed0873eb..fc39494c7 100644 --- a/src/features/Status/StatusPage.tsx +++ b/src/features/Status/StatusPage.tsx @@ -20,6 +20,7 @@ import { import theme from '../../shared/Styles/theme'; import ConfirmationEmailSentComponent from '../Account/ConfirmationEmailSentComponent'; +import { Hacker } from '../../api'; import Background from '../../assets/images/statuspage-background.svg'; export interface IStatusPageProps { @@ -28,7 +29,39 @@ export interface IStatusPageProps { confirmed: boolean; } -class StatusPage extends React.Component { +export interface IStatusPageState { + status: HackerStatus; +} + +class StatusPage extends React.Component { + constructor(props: IStatusPageProps) { + super(props); + this.state = { + status: this.props.status, + }; + } + public confirmStatus = async (e: any) => { + if (this.props.account) { + const hacker = (await Hacker.getByEmail(this.props.account.email)).data + .data; + if (hacker) { + await Hacker.confirm(hacker.id, true); + this.setState({ status: HackerStatus.HACKER_STATUS_CONFIRMED }); + } + } + }; + + public withdrawStatus = async (e: any) => { + if (this.props.account) { + const hacker = (await Hacker.getByEmail(this.props.account.email)).data + .data; + if (hacker) { + await Hacker.confirm(hacker.id, false); + this.setState({ status: HackerStatus.HACKER_STATUS_WITHDRAWN }); + } + } + }; + public render() { return ( @@ -46,7 +79,7 @@ class StatusPage extends React.Component { > Hey {this.props.account.firstName}, - {this.props.status !== HackerStatus.HACKER_STATUS_NONE ? ( + {this.state.status === HackerStatus.HACKER_STATUS_APPLIED ? ( { - ) : Date.now() < CONSTANTS.APPLICATION_CLOSE_TIME ? ( + ) : Date.now() < CONSTANTS.APPLICATION_CLOSE_TIME && + this.state.status === HackerStatus.HACKER_STATUS_NONE ? ( { + ) : this.state.status === HackerStatus.HACKER_STATUS_ACCEPTED ? ( + + + {CONSTANTS.ACCEPTED_STATUS_TEXT} + + + + + + + ) : this.state.status === + HackerStatus.HACKER_STATUS_WAITLISTED ? ( + + + {CONSTANTS.WAITLISTED_STATUS_TEXT} + + + ) : this.state.status === HackerStatus.HACKER_STATUS_DECLINED ? ( + + + {CONSTANTS.DECLINED_STATUS_TEXT} + + + ) : this.state.status === + HackerStatus.HACKER_STATUS_CHECKED_IN ? ( + + + {CONSTANTS.CHECKED_IN_STATUS_TEXT} + + + + + + ) : this.state.status === HackerStatus.HACKER_STATUS_CONFIRMED ? ( + + + {CONSTANTS.CONFIRMED_STATUS_TEXT} + + + + + + + + + ) : this.state.status === HackerStatus.HACKER_STATUS_WITHDRAWN ? ( + + + {CONSTANTS.WITHDRAWN_STATUS_TEXT} + + ) : ( { + constructor(props: {}) { + super(props); + this.state = { + hacker: null, + travel: null, + isLoading: true, + }; + } + public render() { + let reimbursement =
; + if (this.state.travel) { + switch (this.state.travel.status) { + case 'None': + reimbursement = ( +
+ Your request to recieve ${this.state.travel.request.toFixed(2)} in + reimbursement for travel is still being processed. +
+
+

Bus

+ We're offering a round-trip bus from Toronto to McHacks. Seats are + available on a first-come, first-serve basis. You can place a + deposit to secure a seat on the bus{' '} + + here + + . +
+ ); + break; + case 'Bus': + reimbursement = ( +
+ You are taking a bus. We should put more info about the bus here. +
+ ); + break; + case 'Policy': + reimbursement = ( +
+ Your travel reimbursement decision has been released. In order to + see how much you will be reimbursed, you must first agree to our{' '} + + travel policy + + . +
+ +
+
+ ); + break; + case 'Offered': + case 'Valid': + case 'Invalid': + // TODO: Handle Valid and Invalid cases once reciepts are handled + if (this.state.travel.offer > 0) { + reimbursement = ( +
+ We're happy to offer an amount to subsidize your travel to + McHacks. We can reimburse you up to: +

+ ${this.state.travel.offer.toFixed(2)} +

+
+ Please{' '} + + upload your receipts + +
+
+ ); + } else { + reimbursement = ( +
+ Unfortunately, we’re unable to offer you any travel + reimbursement to McHacks. +

+ No Amount +

+

Bus

+ We're offering a round-trip bus from Toronto to McHacks. Seats + are available on a first-come, first-serve basis. You can place + a deposit to secure a seat on the bus{' '} + + here + + . +
+ ); + } + break; + case 'Claimed': + // TODO: Handle Valid and Invalid cases once reciepts are handled + reimbursement = ( +
+ We reimbursed you for +

+ ${this.state.travel.offer.toFixed(2)} +

+ which you have already claimed. +
+ ); + break; + } + } + + return ( +
+ + Travel | {HACKATHON_NAME} + + {this.state.isLoading ? ( +
+ ) : ( + +

+ Travel +

+

Status

+ {reimbursement} +
+
+
+ Please ensure you've reviewed our{' '} + + travel policy + {' '} + if using any of our travel accommodation options. +
+
+ )} + +
+ ); + } + + public componentDidMount() { + return this.getTravelInfo(); + } + + private async getTravelInfo() { + try { + const hacker = (await Hacker.getSelf()).data.data; + const travel = (await Travel.getSelf()).data.data; + this.setState({ + hacker, + travel, + }); + } catch (e) { + if (e && e.data) { + ValidationErrorGenerator(e.data); + } + } finally { + this.setState({ isLoading: false }); + } + } +} + +export default WithToasterContainer(TravelContainer); diff --git a/src/shared/Elements/H2.tsx b/src/shared/Elements/H2.tsx index 74e013de5..35ce8f0ba 100644 --- a/src/shared/Elements/H2.tsx +++ b/src/shared/Elements/H2.tsx @@ -3,6 +3,7 @@ import styled from '../Styles/styled-components'; interface IH2Props { color?: string; fontSize?: string; + fontWeight?: string; textAlign?: string; marginLeft?: string; marginTop?: string; @@ -14,6 +15,7 @@ export const H2 = styled.h2` font-size: ${(props) => props.fontSize || '24px'}; text-align: ${(props) => props.textAlign || 'left'}; color: ${(props) => props.color || props.theme.colors.red}; + font-weight: ${(props) => props.fontWeight || 'bold'}; margin-left: ${(props) => props.marginLeft || 'initial'}; margin-bottom: ${(props) => props.marginBottom || '12px'}; margin-top: ${(props) => props.marginTop || 'initial'}; diff --git a/src/shared/Styles/GlobalStyles.tsx b/src/shared/Styles/GlobalStyles.tsx index 29c303e3c..5d96a5418 100644 --- a/src/shared/Styles/GlobalStyles.tsx +++ b/src/shared/Styles/GlobalStyles.tsx @@ -28,10 +28,10 @@ export const GlobalStyles = createGlobalStyle` } a { - color: ${(props) => props.theme.colors.black80}; + color: ${(props) => props.theme.colors.red}; &:hover { - color: ${(props) => props.theme.colors.black30}; + color: ${(props) => props.theme.colors.redLight}; } diff --git a/src/util/UserInfoHelperFunctions.tsx b/src/util/UserInfoHelperFunctions.tsx index 86bc1e9bc..8fdcec0b4 100644 --- a/src/util/UserInfoHelperFunctions.tsx +++ b/src/util/UserInfoHelperFunctions.tsx @@ -116,14 +116,34 @@ export function canAccessTeam(hacker?: IHacker): boolean { ); } +export function canAccessTravel(hacker?: IHacker): boolean { + const status = hacker ? hacker.status : HackerStatus.HACKER_STATUS_NONE; + + if ( + status === HackerStatus.HACKER_STATUS_APPLIED || + status === HackerStatus.HACKER_STATUS_ACCEPTED || + status === HackerStatus.HACKER_STATUS_CONFIRMED || + status === HackerStatus.HACKER_STATUS_CHECKED_IN + ) { + return !!( + hacker && + hacker.application && + hacker.application.accommodation && + hacker.application.accommodation.travel && + hacker.application.accommodation.travel > 0 + ); + } + return false; +} + export function canAccessBus(hacker?: IHacker): boolean { const status = hacker ? hacker.status : HackerStatus.HACKER_STATUS_NONE; return hacker ? Boolean(hacker.travel) && - (status === HackerStatus.HACKER_STATUS_APPLIED || - status === HackerStatus.HACKER_STATUS_ACCEPTED || - status === HackerStatus.HACKER_STATUS_CONFIRMED || - status === HackerStatus.HACKER_STATUS_CHECKED_IN) + (status === HackerStatus.HACKER_STATUS_APPLIED || + status === HackerStatus.HACKER_STATUS_ACCEPTED || + status === HackerStatus.HACKER_STATUS_CONFIRMED || + status === HackerStatus.HACKER_STATUS_CHECKED_IN) : false; } @@ -147,7 +167,7 @@ export async function generateHackerQRCode(hacker: IHacker): Promise { const hackerPage = ` ${window.location.protocol}//${window.location.hostname}${ window.location.port ? ':' + window.location.port : '' - }${FrontendRoute.VIEW_HACKER_PAGE.replace(':id', hacker.id)}`; + }${FrontendRoute.VIEW_HACKER_PAGE.replace(':id', hacker.id)}`; const response = await QRCode.toDataURL(hackerPage, { scale: 10 }); return response; }