diff --git a/package.json b/package.json index 19fe047f..c0138d8b 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "migrate": "./node_modules/.bin/mm migrate", "rollback": "./node_modules/.bin/mm down", "heroku-postbuild": "npm run migrate && ./node_modules/.bin/gulp prod --production", - "test-server": "tsc -P ./src/server/tsconfig.json --outDir /tmp" + "test-server": "tsc -P ./src/server/tsconfig.json --outDir /tmp", + "lint": "npx tslint --project ." }, "homepage": "https://github.com/UCSDTESC/Check-in#readme", "dependencies": { diff --git a/src/client/components/Fields.tsx b/src/client/components/Fields.tsx index a6c9a354..b8d5f34f 100644 --- a/src/client/components/Fields.tsx +++ b/src/client/components/Fields.tsx @@ -1,5 +1,5 @@ import { UserDiversityOptions, UserYearOptions, UserGenderOptions, UserShirtSizeOptions } from '@Shared/UserEnums'; -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { Field, WrappedFieldProps } from 'redux-form'; import majors from '~/static/Majors.json'; @@ -12,6 +12,44 @@ export type CustomFieldProps = WrappedFieldProps & { maxLength?: number; }; +type ApplicationColProps = { + className?: string; +}; + +type ApplicationLabelProps = { + required?: boolean; + className?: string; + forTag?: string; + // hack? + children?: string | JSX.Element[] | JSX.Element; +}; + +export type ApplicationInputProps = { + className?: string; + name?: string; + placeholder?: string; + type?: string; + normalize?: (value: any, previousValue: any) => void; +}; + +export type ApplicationTextAreaProps = { + name?: string; + placeholder?: string; + maxLength?: number; + className?: string; +}; + +type ApplicationRadioProps = { + name?: string; + value?: any; + label?: string; + className?: string; +}; + +type ApplicationMonthPickerProps = { + name?: string; +}; + /** * Defines all of the custom fields for the application. * Anything beginning with "error" contains a label which renders the error, and @@ -26,6 +64,18 @@ export function createRow(...content: any[]) { ); } +export const ApplicationRow: FunctionComponent = (props) => ( +
+ {props.children} +
+); + +export const ApplicationCol: FunctionComponent = (props) => ( +
+ {props.children} +
+); + export function createColumn(size: string, ...content: any[]) { return (
@@ -34,21 +84,21 @@ export function createColumn(size: string, ...content: any[]) { ); } -export function createError(text: string) { +export const ApplicationError: FunctionComponent<{}> = (props) => { return (
- {text} + {props.children}
); -} +}; export function errorClass(className: string, touched: boolean, error: boolean) { return className + (touched && error ? ' ' + 'sd-form__input--error' : ''); } -export const errorTextInput: React.StatelessComponent = ({ input, className, placeholder, type, - meta: { touched, error } }) => { +export const errorTextInput: FunctionComponent = ({ input, className, placeholder, type, + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return (
@@ -58,12 +108,12 @@ export const errorTextInput: React.StatelessComponent = ({ inp placeholder={placeholder} type={type} /> - {touched && error && createError(error)} + {touched && error && {error}}
); }; -export const errorRadio: React.StatelessComponent = ({ input, className, label, defaultVal }) => { +export const errorRadio: FunctionComponent = ({ input, className, label, defaultVal }) => { return (
); }; -export const errorTShirtSizePicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { +export const errorTShirtSizePicker: FunctionComponent = ({ input, className, type, + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); const sizes = Object.values(UserShirtSizeOptions); const values = Object.keys(UserShirtSizeOptions); @@ -138,13 +188,13 @@ export const errorTShirtSizePicker: React.StatelessComponent = {sizes.map((size, i) => )} - {touched && error && createError(error)} + {touched && error && {error}}
); }; -export const errorGenderPicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { +export const errorGenderPicker: FunctionComponent = ({ input, className, type, + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -157,13 +207,13 @@ export const errorGenderPicker: React.StatelessComponent = ({ {UserGenderOptions.map((gender, i) => )} - {touched && error && createError(error)} + {touched && error && {error}} ); }; -export const errorDiversityOptions: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { +export const errorDiversityOptions: FunctionComponent = ({ input, className, type, + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -176,13 +226,13 @@ export const errorDiversityOptions: React.StatelessComponent = {UserDiversityOptions.map((opt, i) => )} - {touched && error && createError(error)} + {touched && error && {error}} ); }; -export const errorYearPicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { +export const errorYearPicker: FunctionComponent = ({ input, className, type, + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -195,13 +245,13 @@ export const errorYearPicker: React.StatelessComponent = ({ in {UserYearOptions.map((year, i) => )} - {touched && error && createError(error)} + {touched && error && {error}} ); }; -export const errorMajorPicker: React.StatelessComponent = ({ input, className, type, - meta: { touched, error } }) => { +export const errorMajorPicker: FunctionComponent = ({ input, className, type, + meta: { touched, error } }) => { const errorClassName = errorClass(className, touched, error); return ( @@ -214,11 +264,21 @@ export const errorMajorPicker: React.StatelessComponent = ({ i {majors.map((major, i) => )} - {touched && error && createError(error)} + {touched && error && {error}} ); }; +export const ApplicationLabel: FunctionComponent = + ({required = true, className = '', forTag = '', children}) => ( + +); + export function createLabel(text: string, required: boolean = true, className: string = '', forTag: string = '') { return ( @@ -231,6 +291,23 @@ export function createLabel(text: string, required: boolean = true, className: s ); } +export const ApplicationInput: FunctionComponent = ({ + className = 'sd-form__input-text' , + name, + type = 'text', + normalize, + placeholder, +}) => ( + +); + export function createInput(name: string, placeholder: string, type: string = 'text', className: string = 'sd-form__input-text', normalize: (value: any, previousValue: any) => void = null) { @@ -246,9 +323,8 @@ export function createInput(name: string, placeholder: string, type: string = 't ); } -export function createTextArea(name: string, placeholder: string, - maxLength: number = null, className: string = 'sd-form__input-textarea') { - return ( +export const ApplicationTextArea: React.FunctionComponent = + ({className = 'sd-form__input-textarea', name, maxLength = null, placeholder}) => ( - ); -} +); +// TODO: delete after migrating NewEventForm to new Fields API export function createMonthPicker(name: string = null) { return ( = + ({name = null}: ApplicationMonthPickerProps) => ( ); -} -export function createDiversityOptions() { +export const ApplicationGenderPicker: FunctionComponent = () => ( + +); + +export const ApplicationDiversityOptions: FunctionComponent = () => { return ( ); -} +}; -export function createTShirtSizePicker() { - return ( - - ); -} +export const ApplicationTShirtPicker: FunctionComponent = () => ( + +); -export function createYearPicker() { - return ( - - ); -} +export const ApplicationYearPicker: FunctionComponent = () => ( + +); -export function createMajorPicker() { - return ( - - ); -} +export const ApplicationMajorPicker: FunctionComponent = () => ( + +); -export function createRadio(name: string, value: any, label: string, - className: string = 'sd-form__input-radio') { - return ( +export const ApplicationRadio: FunctionComponent = + ({className = 'sd-form__input-radio', name, value, label}) => ( - ); -} +); diff --git a/src/client/components/FileField.tsx b/src/client/components/FileField.tsx index d1ec4f99..ba87939b 100644 --- a/src/client/components/FileField.tsx +++ b/src/client/components/FileField.tsx @@ -2,7 +2,7 @@ import React from 'react'; import Dropzone from 'react-dropzone'; import { WrappedFieldProps } from 'redux-form'; -import * as FormFields from './Fields'; +import { ApplicationError } from './Fields'; interface FileFieldProps { accept?: string; @@ -72,7 +72,7 @@ export default class FileField extends React.Component { )} - {touched && error && FormFields.createError(error)} + {touched && error && {error}} ); } diff --git a/src/client/components/Hero.tsx b/src/client/components/Hero.tsx index f1d4bbee..099fe355 100644 --- a/src/client/components/Hero.tsx +++ b/src/client/components/Hero.tsx @@ -22,9 +22,8 @@ export default class Hero extends React.Component { } return ( -
- -
+ +
); } } diff --git a/src/client/components/User.tsx b/src/client/components/User.tsx index b1d4ffcb..87ce6bb0 100644 --- a/src/client/components/User.tsx +++ b/src/client/components/User.tsx @@ -1,4 +1,5 @@ import { TESCUser, TESCEvent, Question } from '@Shared/ModelTypes'; +import { generateQRCodeURL } from '@Shared/QRCodes'; import { QuestionType } from '@Shared/Questions'; import { getRoleRank, Role } from '@Shared/Roles'; import { isAcceptableStatus, isRejectableStatus, isWaitlistableStatus, UserStatus } from '@Shared/UserStatus'; @@ -9,7 +10,6 @@ import { connect } from 'react-redux'; import { Field, reduxForm, InjectedFormProps } from 'redux-form'; import { sendAcceptanceEmail, sendRejectionEmail, sendWaitlistEmail } from '~/data/AdminApi'; import { ApplicationState } from '~/reducers'; -import { generateQRCodeURL } from '@Shared/QRCodes' import { AlertType } from '../pages/AlertPage'; @@ -114,7 +114,7 @@ class User extends React.Component { className="form-control" component="select" type={fieldType}> - + )}
@@ -343,7 +343,7 @@ class User extends React.Component { {this.renderFormCheckbox('Bussing', 'bussing', 'col-sm-4')} {this.renderFormCheckbox('Sanitized', 'sanitized', 'col-sm-4')} - {this.renderFormDropdown('Status', 'status', + {this.renderFormDropdown('Status', 'status', Object.values(UserStatus), 'col-sm-4')} diff --git a/src/client/data/AdminApi.ts b/src/client/data/AdminApi.ts index 8b9658b9..e7120b96 100644 --- a/src/client/data/AdminApi.ts +++ b/src/client/data/AdminApi.ts @@ -224,7 +224,7 @@ export const downloadResumes = (applicants: string[]): SuperAgentRequest => .post('/resumes') .send({ applicants } as DownloadResumesRequest) .set('Authorization', cookies.get(CookieTypes.admin.token)) - .use(adminApiPrefix) + .use(adminApiPrefix); /** * Requests the status of an ongoing download. @@ -243,12 +243,13 @@ export const pollDownload = (downloadId: string) => * Requests the download of all users as a CSV file. Returned as a request since * it downloads a CSV blob. * @param {String} eventAlias The alias associated with the event. + * @param {boolean} emailsOnly True if only want to export emails. * @returns {Request} A request object not wrapped in a promise. */ -export const exportUsers = (eventAlias: string): SuperAgentRequest => +export const exportUsers = (eventAlias: string, emailsOnly: boolean): SuperAgentRequest => request .post(`/export/`) - .send({alias: eventAlias} as ExportUsersRequest) + .send({alias: eventAlias, emailsOnly: emailsOnly} as ExportUsersRequest) .set('Authorization', cookies.get(CookieTypes.admin.token)) .use(adminApiPrefix); diff --git a/src/client/layouts/public.tsx b/src/client/layouts/public.tsx new file mode 100644 index 00000000..f75a2319 --- /dev/null +++ b/src/client/layouts/public.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import NavHeader from '~/components/NavHeader'; + +class PublicLayout extends React.Component { + render() { + return ( + <> + + {this.props.children} + + ); + } +} + +export default PublicLayout; diff --git a/src/client/layouts/sponsor.tsx b/src/client/layouts/sponsor.tsx index 85d51ca5..7d796eb3 100644 --- a/src/client/layouts/sponsor.tsx +++ b/src/client/layouts/sponsor.tsx @@ -90,7 +90,7 @@ class SponsorLayout extends React.Component { const blob = new Blob([res.text], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); - + link.href = url; link.setAttribute('download', `${this.props.user.username}-${Date.now()}.csv`); document.body.appendChild(link); @@ -98,7 +98,7 @@ class SponsorLayout extends React.Component { hideLoading(); this.setState({ - isDownloading: false + isDownloading: false, }); }) .catch(console.error); diff --git a/src/client/pages/AdminsPage/components/AdminList.tsx b/src/client/pages/AdminsPage/components/AdminList.tsx index 31370393..772b1c14 100644 --- a/src/client/pages/AdminsPage/components/AdminList.tsx +++ b/src/client/pages/AdminsPage/components/AdminList.tsx @@ -24,7 +24,7 @@ export default class AdminList extends React.Component { const {columns} = this.state; - const adminToBeRendered = {...admin, - lastAccessed: admin.lastAccessed - ? moment(admin.lastAccessed).fromNow() - : 'Never Logged In' - } + const adminToBeRendered = {...admin, + lastAccessed: admin.lastAccessed + ? moment(admin.lastAccessed).fromNow() + : 'Never Logged In', + }; return Object.keys(columns).map(column => ( {/* @@ -58,7 +58,7 @@ export default class AdminList extends React.Component) - ) + ); } render() { diff --git a/src/client/pages/ApplyPage/components/PersonalSection.tsx b/src/client/pages/ApplyPage/components/PersonalSection.tsx index 19e46afe..7aba2630 100644 --- a/src/client/pages/ApplyPage/components/PersonalSection.tsx +++ b/src/client/pages/ApplyPage/components/PersonalSection.tsx @@ -2,7 +2,18 @@ import { TESCEvent } from '@Shared/ModelTypes'; import { RegisterUserPersonalSectionRequest } from '@Shared/api/Requests'; import React from 'react'; import { Field, Fields, reduxForm } from 'redux-form'; -import * as FormFields from '~/components/Fields'; +import { + ApplicationRow, + ApplicationCol, + ApplicationLabel, + ApplicationInput, + ApplicationDiversityOptions, + ApplicationError, + ApplicationMajorPicker, + ApplicationYearPicker, + ApplicationMonthPicker, + ApplicationGenderPicker, + errorTextInput } from '~/components/Fields'; import FileField from '~/components/FileField'; import ApplyPageSection, { ApplyPageSectionProps } from './ApplyPageSection'; @@ -33,7 +44,7 @@ class PersonalSection extends ApplyPageSection) => this.props.onEmailChange(e.currentTarget.value)} @@ -95,7 +106,9 @@ class PersonalSection extends ApplyPageSection - {FormFields.createLabel(label, false, 'sd-form__institution-label', id)} + + {label} + ); } @@ -113,7 +126,7 @@ class PersonalSection extends ApplyPageSection{error} ); } @@ -127,9 +140,9 @@ class PersonalSection extends ApplyPageSection + + University ( - ) - ) - ) + )]} + + ); } else if (value === InstitutionType.HighSchool) { return ( - FormFields.createRow( - FormFields.createColumn('col-sm-12', - FormFields.createLabel('High School'), - FormFields.createInput('highSchool', 'High School') - ) - ) + + + High School + + + ); } @@ -167,12 +180,14 @@ class PersonalSection extends ApplyPageSection; } - return (FormFields.createRow( - FormFields.createColumn('col', - FormFields.createLabel('Student PID'), - FormFields.createInput('pid', 'AXXXXXXXX') - ) - )); + return ( + + + Student PID + + + + ); } showMajorYearBoxes(info: any) { @@ -183,16 +198,16 @@ class PersonalSection extends ApplyPageSection + + Major + + + + Year in School + + + ); } @@ -200,20 +215,24 @@ class PersonalSection extends ApplyPageSection + Grade Point Average (GPA) + + + ); } if (requireMajorGPA) { - gpaFields.push(FormFields.createColumn('col-lg-6', - FormFields.createLabel('Major GPA', true), - FormFields.createInput('majorGPA', '4.00') - )); + gpaFields.push( + + Major GPA + + + ); } - return FormFields.createRow(...gpaFields); + return {[...gpaFields]}; } /** @@ -226,30 +245,35 @@ class PersonalSection extends ApplyPageSection - {FormFields.createRow( - FormFields.createColumn('col-sm-12 no-margin-bottom', - FormFields.createLabel('Institution') - ), - FormFields.createColumn('col-md', - this.createInstitutionCard(InstitutionType.UCSD, 'institution-ucsd', - 'UCSD') - ), - FormFields.createColumn('col-md', - this.createInstitutionCard(InstitutionType.University, 'institution-uni', - 'Other University') - ), - allowHighSchool ? FormFields.createColumn('col-md', - this.createInstitutionCard(InstitutionType.HighSchool, - 'institution-hs', 'High School') - ) : '', - FormFields.createColumn('col-sm-12', + + + Institution + + + + {this.createInstitutionCard(InstitutionType.UCSD, 'institution-ucsd', + 'UCSD')} + + + + {this.createInstitutionCard(InstitutionType.University, 'institution-uni', + 'Other University')} + + + {allowHighSchool && + + {this.createInstitutionCard(InstitutionType.HighSchool, + 'institution-hs', 'High School')} + + } + + - ) - )} - + + @@ -257,11 +281,15 @@ class PersonalSection extends ApplyPageSection + + + What is your race / ethnicity? + + + + ); } render() { @@ -270,96 +298,94 @@ class PersonalSection extends ApplyPageSection - {FormFields.createRow( - FormFields.createColumn('col-md-6', - FormFields.createLabel('First Name'), - FormFields.createInput('firstName', 'First Name') - ), - FormFields.createColumn('col-md-6', - FormFields.createLabel('Last Name'), - FormFields.createInput('lastName', 'Last Name') - ) - )} - {FormFields.createRow( - FormFields.createColumn('col-sm-12', - FormFields.createLabel('Email'), - this.createEmailField() - ) - )} - - {FormFields.createRow( - FormFields.createColumn('col-sm-12', - FormFields.createLabel('Birthdate'), + + + First Name + + + + Last Name + + + + + + Email + {this.createEmailField()} + + + + + Birthdate
- {FormFields.createColumn('col-sm-4', - FormFields.createMonthPicker() - )} - {FormFields.createColumn('col-sm-4', - FormFields.createInput('birthdateDay', 'Day', 'number', - 'sd-form__input-text mb-1 mb-md-0') - )} - {FormFields.createColumn('col-sm-4', - FormFields.createInput('birthdateYear', 'Year', 'number', - 'sd-form__input-text') - )} + + + + + + + , + +
- ) - )} - - {FormFields.createRow( - FormFields.createColumn('col-md-6', - FormFields.createLabel('Gender'), - FormFields.createGenderPicker() - ), - FormFields.createColumn('col-md-6', - FormFields.createLabel('Phone Number'), - FormFields.createInput('phone', '555-555-5555', 'text', - 'sd-form__input-text', this.normalizePhone) - ) - )} +
+
+ + + + Gender + + + + Phone Number + + + {this.renderInstitutionOptions(options.allowHighSchool)} - {FormFields.createRow( - FormFields.createColumn('col-lg-6', - FormFields.createLabel('Github Username', false), - FormFields.createInput('github', 'Github') - ), - FormFields.createColumn('col-lg-6', - FormFields.createLabel('Personal Website', false), - FormFields.createInput('website', 'http://example.com/') - ) - )} + + + Github Username' + + + + Personal Website' + + + {this.createGPAFields(options.enableGPA, options.requireGPA, options.requireMajorGPA)} {options.requireDiversityOption && this.createDiversityOptions()} - {FormFields.createRow( - FormFields.createColumn('col-md-4 col-md-offset-4', - FormFields.createLabel('Resume (5MB Max)', options.requireResume), - this.createResumeUpload() - ) - )} - - {FormFields.createRow( - FormFields.createColumn('col-sm-12', - this.createShareCheckbox() - ) - )} - - {FormFields.createRow( - FormFields.createColumn('col-sm-12 text-right', + + + Resume (5MB Max) + {this.createResumeUpload()} + + + + + + {this.createShareCheckbox()} + + + + + - ) - )} + + ); } diff --git a/src/client/pages/ApplyPage/components/ResponseSection.tsx b/src/client/pages/ApplyPage/components/ResponseSection.tsx index 556baf3c..955feea5 100644 --- a/src/client/pages/ApplyPage/components/ResponseSection.tsx +++ b/src/client/pages/ApplyPage/components/ResponseSection.tsx @@ -3,7 +3,17 @@ import { QuestionType } from '@Shared/Questions'; import { RegisterUserResponseSectionRequest } from '@Shared/api/Requests'; import React from 'react'; import { Fields, reduxForm, Field, WrappedFieldProps } from 'redux-form'; -import * as FormFields from '~/components/Fields'; +import { + ApplicationRow, + ApplicationCol, + ApplicationLabel, + ApplicationInput, + ApplicationTextArea, + ApplicationRadio, + ApplicationTextAreaProps, + ApplicationInputProps, + ApplicationError, + ApplicationTShirtPicker } from '~/components/Fields'; import { createTeamCode } from '~/data/UserApi'; import ApplyPageSection, { ApplyPageSectionProps } from './ApplyPageSection'; @@ -35,12 +45,12 @@ class ResponseSection extends ApplyPageSection + + If yes, from where? + + + ); } return ; @@ -48,29 +58,34 @@ class ResponseSection extends ApplyPageSection JSX.Element | JSX.Element[] = null; + let InputField: React.FunctionComponent< + { name: string } + | ApplicationTextAreaProps + | ApplicationInputProps + > = null; switch (type) { case QuestionType.QUESTION_LONG: - inputField = FormFields.createTextArea; + InputField = ApplicationTextArea; break; case QuestionType.QUESTION_SHORT: - inputField = FormFields.createInput; + InputField = ApplicationInput; break; case QuestionType.QUESTION_CHECKBOX: - inputField = (name: string) => [ - FormFields.createRadio(name, true, 'Yes'), - FormFields.createRadio(name, false, 'No'), - ]; + InputField = ({name}) => ( + <> + + + + ); break; } return customQuestions[type].map(x => ( - FormFields.createColumn('col-sm-12', - FormFields.createLabel(x.question, x.isRequired), - inputField(`customQuestionResponses.${x._id}`, - 'Your Response...') - ) + + {x.question} + {} + )); } @@ -99,7 +114,9 @@ class ResponseSection extends ApplyPageSection - {FormFields.createLabel(label, false, 'sd-form__team-label', id)} + + {label} + ); } @@ -115,7 +132,7 @@ class ResponseSection extends ApplyPageSection{error} ); } @@ -127,24 +144,24 @@ class ResponseSection extends ApplyPageSection - {FormFields.createRow( - FormFields.createColumn('col-sm-12 no-margin-bottom', - FormFields.createLabel('Create or Join a Team') - ), - FormFields.createColumn('col-md', - this.createTeamStateCard(JoinCreateTeamState.CREATE, 'create-team', - 'Create') - ), - FormFields.createColumn('col-md', - this.createTeamStateCard(JoinCreateTeamState.JOIN, 'join-team', - 'Join') - ), - FormFields.createColumn('col-sm-12', + + + Create or Join a Team + + + {this.createTeamStateCard(JoinCreateTeamState.CREATE, 'create-team', + 'Create')} + + + {this.createTeamStateCard(JoinCreateTeamState.JOIN, 'join-team', + 'Join')} + + - ), + createTeamCode(event._id), } as TeamRegisterProps} /> - )} + ); } @@ -164,80 +181,95 @@ class ResponseSection extends ApplyPageSection - {options.foodOption && FormFields.createRow( - FormFields.createColumn('col-sm-12', - FormFields.createLabel('What kind of food would you like to see ' + - 'at the hackathon?', false), - FormFields.createTextArea('food', 'Healthy Snacks and Drinks') - ) - )} - - {FormFields.createRow( - FormFields.createColumn('col-sm-12', - FormFields.createLabel('Dietary Restrictions', false), - FormFields.createTextArea('diet', 'Dietary Restrictions') - ) - )} + {options.foodOption && + + + + What kind of food would you like to see at the event? + + + + } + + + Dietary Restrictions + + + {options.requireWhyThisEvent && - FormFields.createColumn('col-sm-12', - FormFields.createLabel(`Why Do You Want To Attend ${event.name}?`, true), - FormFields.createTextArea('whyEventResponse', 'Your Response...') - ) + + + <>Why Do You Want To Attend {event.name}? + + + } - {options.allowOutOfState && FormFields.createRow( - FormFields.createColumn('col-lg-12', - FormFields.createLabel('I will be travelling from outside the ' + - 'San Diego county'), - FormFields.createRadio('outOfState', true, 'Yes'), - FormFields.createRadio('outOfState', false, 'No') - ) - )} + {options.allowOutOfState && + + + I will be travelling from outside the San Diego county + + + + + } {options.allowOutOfState && } - {options.requireExtraCurriculars && FormFields.createRow( - FormFields.createColumn('col-sm-12', - FormFields.createLabel('Please put down any extra curriculars or Student' - + ' Organizations you are affiliated with', true), - FormFields.createTextArea('extraCurriculars', 'Extra Curriculars') - ) - )} + {options.requireExtraCurriculars && + + + + Please put down any extra curriculars or Student Organizations you are affiliated with + + + + + } - {FormFields.createRow( - FormFields.createColumn('col-12', - FormFields.createLabel('T-Shirt Size (Unisex)'), - FormFields.createTShirtSizePicker() - ) - )} + + + T-Shirt Size (Unisex) + + + - {customQuestions && FormFields.createRow( - this.renderCustomQuestions(customQuestions, - QuestionType.QUESTION_LONG))} + {customQuestions && + {this.renderCustomQuestions(customQuestions, + QuestionType.QUESTION_LONG)} + } - {options.requireClassRequirement && FormFields.createRow( - FormFields.createColumn('col-lg-12', - FormFields.createLabel('Have you taken an Advanced Data Structures ' + - '(CSE 100) or equivalent class?'), - FormFields.createRadio('classRequirement', true, 'Yes'), - FormFields.createRadio('classRequirement', false, 'No') - ) - )} + {options.requireClassRequirement && + + + + Have you taken an Advanced Data Structures (CSE 100) or equivalent class? + + + + + + } - {customQuestions && FormFields.createRow( - this.renderCustomQuestions(customQuestions, - QuestionType.QUESTION_SHORT))} + {customQuestions && + + {this.renderCustomQuestions(customQuestions, + QuestionType.QUESTION_SHORT)} + } - {customQuestions && FormFields.createRow( - this.renderCustomQuestions(customQuestions, - QuestionType.QUESTION_CHECKBOX))} + {customQuestions && + + {this.renderCustomQuestions(customQuestions, + QuestionType.QUESTION_CHECKBOX)} + } {options.allowTeammates && this.renderTeamOptions(this.state.teamState)} - {FormFields.createRow( - FormFields.createColumn('col-sm-12 col-md-4 text-center', + + - ), - FormFields.createColumn('col-sm-12 col-md-8 text-right', + + - ) - )} + + ); } diff --git a/src/client/pages/ApplyPage/components/UniversityField.tsx b/src/client/pages/ApplyPage/components/UniversityField.tsx index 2e2ed6f0..77c12ee9 100644 --- a/src/client/pages/ApplyPage/components/UniversityField.tsx +++ b/src/client/pages/ApplyPage/components/UniversityField.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { WrappedFieldProps } from 'redux-form'; import AutoSuggest from '~/components/AutoSuggest'; -import * as FormFields from '~/components/Fields'; +import { ApplicationError } from '~/components/Fields'; import { getSuggestions } from '~/static/Universities'; interface UniversityFieldProps { @@ -35,7 +35,7 @@ export default class UniversityField extends React.Component { onSuggestionSelected={this.onUniversitySelected} minChars={3} /> - {touched && error && FormFields.createError(error)} + {touched && error && {error}} ); } diff --git a/src/client/pages/ApplyPage/components/UserSection.tsx b/src/client/pages/ApplyPage/components/UserSection.tsx index 414710bb..b67e603c 100644 --- a/src/client/pages/ApplyPage/components/UserSection.tsx +++ b/src/client/pages/ApplyPage/components/UserSection.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Field, reduxForm } from 'redux-form'; -import * as FormFields from '~/components/Fields'; +import { ApplicationRow, ApplicationCol, ApplicationLabel, ApplicationInput, ApplicationError } from '~/components/Fields'; import ApplyPageSection, { ApplyPageSectionProps } from './ApplyPageSection'; @@ -8,6 +8,7 @@ interface UserSectionProps extends ApplyPageSectionProps { emailExists: boolean; submitError: Error; isSubmitting: boolean; + previewMode: boolean; } export interface UserSectionFormData { @@ -55,20 +56,21 @@ class UserSection extends ApplyPageSection - {FormFields.createRow( - FormFields.createColumn('col-sm-12', - FormFields.createLabel(`I authorize you to share my - application/registration information for event administration, + + + + I authorize you to share my application/registration information for event administration, ranking, MLH administration, pre- and post-event informational e-mails, and occasional messages about hackathons in-line with the MLH Privacy Policy. I further I agree to the terms of both the MLH - Contest Terms and Conditions and the MLH Privacy Policy.`) - ) - )} + Contest Terms and Conditions and the MLH Privacy Policy. + + + - {FormFields.createRow( - FormFields.createColumn('col-sm-12', - this.createProvisionBox(), + + + {this.createProvisionBox()} I agree to the  . - ), - FormFields.createColumn('col-sm-12', - this.createAcceptBox(), + + + {this.createAcceptBox()} I have read and agree to the  - ) - )} + + ); } render() { - const { goToPreviousPage, handleSubmit, pristine, isSubmitting, submitError, emailExists } = this.props; + const { goToPreviousPage, handleSubmit, pristine, isSubmitting, submitError, emailExists, previewMode } = this.props; const options = this.props.event.options; return (
- {emailExists && FormFields.createRow( - FormFields.createColumn('col-sm-12 mt-4 text-center', + {emailExists && +

You're Almost Done!

,
We see you already have a TESC Events account
We will link this application to that -
- ) - )} + +
+
} - {!emailExists && FormFields.createRow( - FormFields.createColumn('col-sm-12', + {!emailExists && +

You're Almost Done!

,
To complete your application, please add a password -
- ), - FormFields.createColumn('col-md-6', - FormFields.createLabel('Password'), - FormFields.createInput('password', 'Password', 'password') - ), - FormFields.createColumn('col-md-6', - FormFields.createLabel('Confirm Password'), - FormFields.createInput('confirmPassword', 'Confirm Password', 'password') - ) - )} + +
+ + Password + + + + Confirm Password + + +
} {options.mlhProvisions && this.createMLHProvisions()} - {FormFields.createRow( - FormFields.createColumn('col-sm-12 col-md-4 text-center', - - ), - FormFields.createColumn('col-sm-12 col-md-8 text-right', - - ), - FormFields.createColumn('col-sm-12 col-md-4 text-center', - - {isSubmitting && } - - ) - )} + + + + + + + + + + {isSubmitting && } + + + - {submitError && FormFields.createRow( - FormFields.createColumn('col-sm-12', - // TODO: Handle special cases of submits - check to see if it actually is resume problems - FormFields.createError(submitError.message) - ) - )} + {submitError && + + {submitError.message} + + } ); } diff --git a/src/client/pages/ApplyPage/index.tsx b/src/client/pages/ApplyPage/index.tsx index 0ef27a8a..18488f4e 100644 --- a/src/client/pages/ApplyPage/index.tsx +++ b/src/client/pages/ApplyPage/index.tsx @@ -5,7 +5,6 @@ import Progress from 'react-progress'; import { withRouter, RouteComponentProps } from 'react-router-dom'; import { Link } from 'react-router-dom'; import Loading from '~/components/Loading'; -import NavHeader from '~/components/NavHeader'; import { registerUser, loadEventByAlias, checkUserExists } from '~/data/UserApi'; import Header from './components/Header'; @@ -16,9 +15,10 @@ import UserSection, { UserSectionFormData } from './components/UserSection'; import createValidator from './validate'; interface ApplyPageProps { + previewMode?: boolean; } -type Props = RouteComponentProps<{ +type Props = ApplyPageProps & RouteComponentProps<{ eventAlias: string; }>; @@ -143,6 +143,9 @@ class ApplyPage extends React.Component { * @param {Object} values The validated form data. */ onFinalSubmit = (values: ApplyPageFormData) => { + + const {previewMode} = this.props; + values = this.sanitiseValues(values); this.setState({ @@ -203,11 +206,11 @@ class ApplyPage extends React.Component { render() { const {page, event, emailExists} = this.state; + const {previewMode} = this.props; if (!event) { return (
-
); @@ -217,10 +220,9 @@ class ApplyPage extends React.Component { const validator = createValidator(options, event.customQuestions); // Check for closed - if (new Date(event.closeTime) < new Date()) { + if (!previewMode && new Date(event.closeTime) < new Date()) { return (
-
@@ -250,7 +252,6 @@ class ApplyPage extends React.Component { return (
-
@@ -279,6 +280,7 @@ class ApplyPage extends React.Component { validate={validator} event={event} emailExists={emailExists} + previewMode={previewMode} />} {page === 4 && }
diff --git a/src/client/pages/EventPage/components/PreviewApplication.tsx b/src/client/pages/EventPage/components/PreviewApplication.tsx new file mode 100644 index 00000000..0f1013a2 --- /dev/null +++ b/src/client/pages/EventPage/components/PreviewApplication.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { Alert } from 'reactstrap'; + +import ApplyPage from '../../ApplyPage'; + +class PreviewApplication extends React.Component { + + render() { + return ( + <> + + You are in preview mode. + + + + ); + } +} + +export default PreviewApplication; diff --git a/src/client/pages/EventPage/components/ViewApplication.tsx b/src/client/pages/EventPage/components/ViewApplication.tsx new file mode 100644 index 00000000..f70a901f --- /dev/null +++ b/src/client/pages/EventPage/components/ViewApplication.tsx @@ -0,0 +1,53 @@ +import { TESCEvent } from '@Shared/ModelTypes'; +import React from 'react'; +import FA from 'react-fontawesome'; +import { Link } from 'react-router-dom'; +import { UncontrolledTooltip } from 'reactstrap/lib/Uncontrolled'; + +type ViewApplicationProps = { + event: TESCEvent; +}; + +class ViewApplication extends React.Component { + + render() { + const {event} = this.props; + const isEventClosed = new Date(event.closeTime) < new Date; + return ( + <> + {isEventClosed && + Preview Application + + + + + + This event is currently not accepting applications. + You can preview the form before making it public. + + + } + + {!isEventClosed && + Go To Form + } + + ); + } +} + +export default ViewApplication; diff --git a/src/client/pages/EventPage/index.tsx b/src/client/pages/EventPage/index.tsx index cda9b170..f60aef81 100644 --- a/src/client/pages/EventPage/index.tsx +++ b/src/client/pages/EventPage/index.tsx @@ -21,6 +21,7 @@ import { } from './actions'; import CheckinStatistics from './components/CheckinStatistics'; import ResumeStatistics from './components/ResumeStatistics'; +import ViewApplication from './components/ViewApplication'; import ActionsTab from './tabs/ActionsTab'; import AdministratorsTab from './tabs/AdministratorsTab'; import SettingsTab from './tabs/SettingsTab'; @@ -258,13 +259,7 @@ class EventPage extends TabularPage { className="d-none d-md-block" /> - - Go To Form - +
diff --git a/src/client/pages/EventPage/tabs/ActionsTab.tsx b/src/client/pages/EventPage/tabs/ActionsTab.tsx index edf20b7b..14777634 100644 --- a/src/client/pages/EventPage/tabs/ActionsTab.tsx +++ b/src/client/pages/EventPage/tabs/ActionsTab.tsx @@ -11,7 +11,23 @@ interface ActionsTabProps { export default class ActionsTab extends EventPageTab { exportUsers = () => { const eventAlias = this.props.event.alias; - exportUsers(eventAlias) + exportUsers(eventAlias, false) + .end((err, res) => { + // Download as file + const blob = new Blob([res.text], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `${eventAlias}-${Date.now()}.csv`); + document.body.appendChild(link); + + link.click(); + }); + } + + exportEmails = () => { + const eventAlias = this.props.event.alias; + exportUsers(eventAlias, true) .end((err, res) => { // Download as file const blob = new Blob([res.text], { type: 'text/csv;charset=utf-8;' }); @@ -55,6 +71,15 @@ export default class ActionsTab extends EventPageTab { > Export All Users
+ + + Export User Emails + +
diff --git a/src/client/pages/UsersPage/components/UserList.tsx b/src/client/pages/UsersPage/components/UserList.tsx index 6c884360..bf971e4a 100644 --- a/src/client/pages/UsersPage/components/UserList.tsx +++ b/src/client/pages/UsersPage/components/UserList.tsx @@ -36,7 +36,7 @@ class UserList extends React.Component { data={this.props.users} column={{ ...ReactTableDefaults.column, - Cell: ({value}) => value ? String(value): value + Cell: ({value}) => value ? String(value): value, }} columns={this.props.columns} defaultPageSize={10} diff --git a/src/client/routes.tsx b/src/client/routes.tsx index 9fea7cb8..e6302f11 100644 --- a/src/client/routes.tsx +++ b/src/client/routes.tsx @@ -22,6 +22,7 @@ import ConfirmPage from './auth/user/Confirm'; import UserLogout from './auth/user/Logout'; import { authoriseUser, finishAuthorisation as finishUserAuth, logoutUser } from './auth/user/actions'; import AdminLayout from './layouts/admin'; +import PublicLayout from './layouts/public'; import SponsorLayout from './layouts/sponsor'; import UserLayout from './layouts/user'; import AdminsPage from './pages/AdminsPage'; @@ -29,6 +30,7 @@ import ApplyPage from './pages/ApplyPage'; import CheckinPage from './pages/CheckinPage'; import Dashboard from './pages/DashboardPage'; import EventPage from './pages/EventPage'; +import PreviewApplication from './pages/EventPage/components/PreviewApplication'; import ForgotPage from './pages/ForgotPage'; import HomePage from './pages/HomePage'; import LoginPage from './pages/LoginPage'; @@ -147,17 +149,26 @@ class Routes extends React.Component { ); } + renderPublic = (RenderComponent: any) => { + return (props: RouteComponentProps) => + ( + + + + ); + } + routes() { return ( { {/* Event Specific Routes */} + user.csvFlatten()); + + var flattenedUsers = eventUsers.map(user => user.csvFlatten(false, body.emailsOnly)); const fileName = `${event.alias}-${moment().format()}.csv`; const csv = this.CSVService.parseJSONToCSV(flattenedUsers); response = this.CSVService.setJSONReturnHeaders(response, fileName); return response.send(csv); } -} +} \ No newline at end of file diff --git a/src/server/models/User.ts b/src/server/models/User.ts index 33e50855..de3c23b8 100644 --- a/src/server/models/User.ts +++ b/src/server/models/User.ts @@ -11,7 +11,7 @@ import { print } from 'util'; import { generateQRCodeURL } from '@Shared/QRCodes'; export type UserDocument = TESCUser & Document & { - csvFlatten: (isSponsor? : boolean) => any; + csvFlatten: (isSponsor? : boolean, emailsOnly? : boolean) => any; attach: (name: string, options: any) => Promise; }; export type UserModel = Model; @@ -273,7 +273,7 @@ UserSchema.plugin(crate, { }, }); -UserSchema.method('csvFlatten', function (isSponsor = false) { +UserSchema.method('csvFlatten', function (isSponsor = false, emailsOnly = false) { // tslint:disable-next-line:no-invalid-this no-this-assignment const user = this; let autoFill = ['_id', 'firstName', 'lastName', 'email', 'birthdate', @@ -286,26 +286,32 @@ UserSchema.method('csvFlatten', function (isSponsor = false) { 'university', 'major', 'year', 'github', 'website', 'gpa', 'majorGPA']; } + if (emailsOnly) { + autoFill = ['firstName', 'lastName', 'email']; + } + let autoFilled: any = autoFill.reduce((acc, val) => { return Object.assign(acc, { [val]: user[val] }); }, {}); - autoFilled.outOfState = user.travel.outOfState; - autoFilled.city = user.travel.city; - autoFilled.resume = user.resume ? user.resume.url : ''; - autoFilled.email = user.account ? user.account.email : ''; - if (!isSponsor) { - autoFilled.whyEvent = user.whyEventResponse ? user.whyEventResponse : ''; - - if (user.customQuestionResponses) { - autoFilled = {...autoFilled, ...user.customQuestionResponses.toJSON()}; + if (!emailsOnly) { + autoFilled.outOfState = user.travel.outOfState; + autoFilled.city = user.travel.city; + autoFilled.resume = user.resume ? user.resume.url : ''; + + if (!isSponsor) { + autoFilled.whyEvent = user.whyEventResponse ? user.whyEventResponse : ''; + + if (user.customQuestionResponses) { + autoFilled = {...autoFilled, ...user.customQuestionResponses.toJSON()}; + } + + autoFilled.team = user.team ? user.team.code : ''; + + autoFilled.qrCode = generateQRCodeURL(user); } - - autoFilled.team = user.team ? user.team.code : ''; - - autoFilled.qrCode = generateQRCodeURL(user); } return autoFilled; @@ -326,4 +332,4 @@ export const EDITABLE_USER_FIELDS: string[] = Object.entries((UserSchema as any) EDITABLE_USER_FIELDS.push('resume'); export const RegisterModel = () => - Container.set('UserModel', model('User', UserSchema)); + Container.set('UserModel', model('User', UserSchema)); \ No newline at end of file diff --git a/src/shared/QRCodes.ts b/src/shared/QRCodes.ts index 74e80741..40f62f53 100644 --- a/src/shared/QRCodes.ts +++ b/src/shared/QRCodes.ts @@ -1,10 +1,10 @@ import { TESCUser } from '@Shared/ModelTypes'; const QR_CODE_SIZE = 200; -const QR_CODE_API_ROOT = `https://api.qrserver.com/v1/create-qr-code/` +const QR_CODE_API_ROOT = `https://api.qrserver.com/v1/create-qr-code/`; function generateQRCodeURL(user: TESCUser) { - return `${QR_CODE_API_ROOT}?size=${QR_CODE_SIZE}x${QR_CODE_SIZE}&data=${user._id}` + return `${QR_CODE_API_ROOT}?size=${QR_CODE_SIZE}x${QR_CODE_SIZE}&data=${user._id}`; } -export { generateQRCodeURL } \ No newline at end of file +export { generateQRCodeURL }; diff --git a/src/shared/api/Requests.ts b/src/shared/api/Requests.ts index fb76ba90..0a96084f 100644 --- a/src/shared/api/Requests.ts +++ b/src/shared/api/Requests.ts @@ -48,6 +48,7 @@ export interface DownloadResumesRequest { export interface ExportUsersRequest { alias: string; + emailsOnly: boolean; } export interface ForgotPasswordRequest {