diff --git a/.github/ISSUE_TEMPLATE/user_research_task.md b/.github/ISSUE_TEMPLATE/user_research_task.md index 11ecbc68e..e22336b1f 100644 --- a/.github/ISSUE_TEMPLATE/user_research_task.md +++ b/.github/ISSUE_TEMPLATE/user_research_task.md @@ -1,7 +1,7 @@ --- name: User research about: Prepare some user research we need to do with our users - mentors, mentees, - jobseekers, companies and recuriters + jobSeekers, companies and recuriters title: "[Name of epic:] A brief title" labels: User research assignees: '' diff --git a/README.md b/README.md index 2b34dd64e..596fd75f7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ You'll find two sister products in this repository: - ReDI Connect, a tool to connect mentees to mentors, deployed to https://connect.redi-school.org -- ReDI Talent Pool, a tool to connect jobseekers to companies and get jobs, deployed to https://cv-builder.redi-school.org +- ReDI Talent Pool, a tool to connect jobSeekers to companies and get jobs, deployed to https://cv-builder.redi-school.org Both are created, run and managed by ReDI School of Digital Integration. We're a non-profit school in Germany (in Berlin, Munich and NRW) with a community of hundreds of professionals from the digital industry volunteering to teach and mentor students. Our students are tech-interested locals and newcomers to Germany. diff --git a/apps/admin-panel/src/App.jsx b/apps/admin-panel/src/App.jsx deleted file mode 100644 index c5b143472..000000000 --- a/apps/admin-panel/src/App.jsx +++ /dev/null @@ -1,2341 +0,0 @@ -import React, { useEffect } from 'react' -import { get, mapValues, keyBy, groupBy } from 'lodash' -import { - Admin, - Resource, - List, - Tab, - Create, - Pagination, - Filter, - Datagrid, - TabbedForm, - FunctionField, - TabbedShowLayout, - TextField, - ReferenceInput, - AutocompleteInput, - DateField, - TextInput, - BooleanInput, - NullableBooleanInput, - NumberField, - FormTab, - NumberInput, - Show, - ShowButton, - LongTextInput, - CardActions, - DateInput, - EditButton, - SelectInput, - Edit, - SimpleForm, - ArrayField, - BooleanField, - SimpleShowLayout, - SelectArrayInput, - downloadCSV, - ReferenceField, - Labeled, - ReferenceManyField, - SearchInput, -} from 'react-admin' -import classNames from 'classnames' -import { unparse as convertToCSV } from 'papaparse/papaparse.min' -import { createStyles, withStyles } from '@material-ui/core' -import { Person as PersonIcon } from '@material-ui/icons' -import Button from '@material-ui/core/Button' -import Table from '@material-ui/core/Table' -import TableBody from '@material-ui/core/TableBody' -import TableCell from '@material-ui/core/TableCell' -import TableHead from '@material-ui/core/TableHead' -import TableRow from '@material-ui/core/TableRow' -import FormControl from '@material-ui/core/FormControl' -import InputLabel from '@material-ui/core/InputLabel' -import Select from '@material-ui/core/Select' -import MenuItem from '@material-ui/core/MenuItem' -import Typography from '@material-ui/core/Typography' -import Card from '@material-ui/core/Card' -import CardContent from '@material-ui/core/CardContent' -import DateFnsUtils from '@date-io/date-fns' - -import { - MuiPickersUtilsProvider, - KeyboardDatePicker, -} from '@material-ui/pickers' - -import { - REDI_LOCATION_NAMES, - LANGUAGES, - CATEGORY_GROUPS, - CATEGORIES, - COURSES, - GENDERS, - MENTORING_SESSION_DURATION_OPTIONS, - RED_MATCH_STATUSES, -} from '@talent-connect/shared-config' - -import { calculateAge } from '@talent-connect/shared-utils' - -import { howDidHearAboutRediOptions } from '@talent-connect/talent-pool/config' - -import loopbackClient, { authProvider } from './lib/react-admin-loopback/src' -import { ApproveButton } from './components/ApproveButton' -import { DeclineButton } from './components/DeclineButton' -import { TpJobseekerProfileApproveButton } from './components/TpJobseekerProfileApproveButton' -import { TpJobseekerProfileDeclineButton } from './components/TpJobseekerProfileDeclineButton' -import { TpCompanyProfileApproveButton } from './components/TpCompanyProfileApproveButton' - -import { API_URL } from './config' -import { - TpJobseekerProfileState, - TpCompanyProfileState, -} from '@talent-connect/shared-types' - -import { objectEntries } from '@talent-connect/typescript-utilities' - -import { redMatchesCsvExporter } from './utils/csvExport' - -/** REFERENCE DATA */ - -const rediLocations = objectEntries(REDI_LOCATION_NAMES).map(([id, label]) => ({ - id, - label, -})) - -const categoriesFlat = CATEGORIES.map((cat) => ({ - ...cat, - labelClean: cat.label, - label: `${cat.label} (${cat.group})`, -})) - -const coursesByLocation = groupBy(COURSES, 'location') -const coursesFlat = [ - ...coursesByLocation.berlin.map((cat) => - Object.assign(cat, { label: `Berlin: ${cat.label}` }) - ), - ...coursesByLocation.munich.map((cat) => - Object.assign(cat, { label: `Munich: ${cat.label}` }) - ), - ...coursesByLocation.nrw.map((cat) => - Object.assign(cat, { label: `NRW: ${cat.label}` }) - ), -] - -const categoriesIdToLabelCleanMap = mapValues( - keyBy(categoriesFlat, 'id'), - 'labelClean' -) -const categoriesIdToGroupMap = mapValues(keyBy(categoriesFlat, 'id'), 'group') - -const genders = [ - ...Object.entries(GENDERS).map((key, value) => ({ id: key, name: value })), - { id: '', name: 'Prefers not to answer' }, -] - -const languages = LANGUAGES.map((lang) => ({ id: lang, name: lang })) - -const courseIdToLabelMap = mapValues(keyBy(coursesFlat, 'id'), 'label') -const AWS_PROFILE_AVATARS_BUCKET_BASE_URL = - 'https://s3-eu-west-1.amazonaws.com/redi-connect-profile-avatars/' - -export const formRedMatchStatuses = Object.entries(RED_MATCH_STATUSES).map( - ([key, value]) => ({ id: key, name: value }) -) - -/** START OF SHARED STUFF */ - -const RecordCreatedAt = (props) => -RecordCreatedAt.defaultProps = { - addLabel: true, - label: 'Record created at', -} - -const RecordUpdatedAt = (props) => -RecordUpdatedAt.defaultProps = { - addLabel: true, - label: 'Record updated at', -} - -const LanguageList = (props) => { - return {props.data ? Object.values(props.data).join(', ') : null} -} - -const CategoryList = (props) => { - const categoriesGrouped = groupBy( - props.data, - (catId) => categoriesIdToGroupMap[catId] - ) - return ( - <> - {Object.keys(categoriesGrouped).map((groupId, index) => ( - - - {CATEGORY_GROUPS[groupId]}:{' '} - {categoriesGrouped[groupId] - .map((catId) => categoriesIdToLabelCleanMap[catId]) - .join(', ')} - -
-
- ))} - - ) -} - -const styles = createStyles({ - avatarImage: { - width: '500px', - height: '500px', - backgroundSize: 'cover', - backgroundPosition: 'center center', - }, -}) - -const Avatar = withStyles(styles)(({ record, className, classes, style }) => ( - <> - {!record && ( - - )} - {record && record.profileAvatarImageS3Key && ( -
- )} - -)) - -/** END OF SHARED STUFF */ -const AllModelsPagination = (props) => ( - -) - -const RedProfileList = (props) => { - return ( - } - pagination={} - aside={} - exporter={redProfileListExporter} - > - }> - - - - - ; - - - - - - - - - - - - - ) -} - -function redProfileListExporter(profiles) { - const properties = [ - 'id', - 'userType', - 'rediLocation', - 'firstName', - 'lastName', - 'gender', - 'age', - 'birthDate', - 'userActivated', - 'userActivatedAt', - 'mentor_occupation', - 'mentor_workPlace', - 'expectations', - 'mentor_ifTypeForm_submittedAt', - 'mentee_ifTypeForm_preferredMentorSex', - 'mentee_currentCategory', - 'mentee_occupationCategoryId', - 'mentee_occupationJob_placeOfEmployment', - 'mentee_occupationJob_position', - 'mentee_occupationStudent_studyPlace', - 'mentee_occupationStudent_studyName', - 'mentee_occupationLookingForJob_what', - 'mentee_occupationOther_description', - 'mentee_highestEducationLevel', - 'mentee_currentlyEnrolledInCourse', - 'profileAvatarImageS3Key', - 'languages', - 'otherLanguages', - 'personalDescription', - 'contactEmail', - 'linkedInProfileUrl', - 'githubProfileUrl', - 'slackUsername', - 'telephoneNumber', - 'categories', - 'favouritedRedProfileIds', - 'optOutOfMenteesFromOtherRediLocation', - 'signupSource', - 'currentApplicantCount', - 'menteeCountCapacity', - 'currentMenteeCount', - 'currentFreeMenteeSpots', - 'ifUserIsMentee_hasActiveMentor', - 'ifUserIsMentee_activeMentor', - 'ifTypeForm_additionalComments', - 'createdAt', - 'updatedAt', - 'gaveGdprConsentAt', - 'administratorInternalComment', - ] - - const data = profiles.map((profile) => { - return Object.fromEntries(properties.map((prop) => [prop, profile[prop]])) - }) - - const csv = convertToCSV(data) - downloadCSV(csv, 'yalla') -} - -const FreeMenteeSpotsPerLocationAside = () => { - const [mentorsList, setMentorsList] = React.useState([]) - - useEffect(() => { - dataProvider('GET_LIST', 'redProfiles', { - pagination: { page: 1, perPage: 0 }, - sort: {}, - filter: { userType: 'mentor' }, - }).then(({ data }) => setMentorsList(data)) - }, []) - - const getFreeSpotsCount = (location) => - mentorsList - .filter((mentor) => mentor.rediLocation === location) - .filter((mentor) => mentor.userActivated) - .reduce((acc, curr) => acc + curr.currentFreeMenteeSpots, 0) - - const totalFreeMenteeSpotsBerlin = getFreeSpotsCount('berlin') - const totalFreeMenteeSpotsMunich = getFreeSpotsCount('munich') - const totalFreeMenteeSpotsNRW = getFreeSpotsCount('nrw') - - return ( -
- - - Free Mentee Spots Per Location - - Berlin: {totalFreeMenteeSpotsBerlin} mentoring spots available - - - Munich: {totalFreeMenteeSpotsMunich} mentoring spots available - - - NRW: {totalFreeMenteeSpotsNRW} mentoring spots available - - - -
- ) -} - -const RedProfileListExpandPane = (props) => { - return ( - - - - - - - - - - - - ) -} - -const RedProfileListFilters = (props) => ( - - - ({ id, name: label }))} - /> - - ({ id, name: label }))} - /> - ({ id, name: label }))} - > - - -) -function userTypeToEmoji({ userType }) { - const emoji = { - mentor: '🎁 Mentor', - mentee: 'πŸ’Ž Mentee', - 'public-sign-up-mentor-pending-review': 'πŸ’£ Mentor (pending review)', - 'public-sign-up-mentee-pending-review': '🧨 Mentee (pending review)', - 'public-sign-up-mentor-rejected': '❌🎁 Mentor (rejected)', - 'public-sign-up-mentee-rejected': 'βŒπŸ’ŽMentee (rejected)', - }[userType] - return emoji ?? userType -} - -const RedProfileShow = (props) => ( - - - - - - - - - - - - calculateAge(person.birthDate)} - /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Mentor-specific fields:

- - - -

Mentee-specific fields:

- - - - - - - - - -

Record information

- - - -

- Typeform information (for mentors/mentees originally signed up via - typeform) -

- - - -
- - - -
-
-
-) - -const RedProfileEditActions = (props) => { - const userType = props && props.data && props.data.userType - if ( - ![ - 'public-sign-up-mentor-pending-review', - 'public-sign-up-mentee-pending-review', - ].includes(userType) - ) { - return null - } - return ( - - User is pending. Please - or - - - ) -} - -const RedProfileEdit = (props) => ( - }> - - - - - - - - - - - - - - - - - - - - - - - - - - - - -) - -const CategoriesInput = (props) => { - const categories = categoriesFlat - return ( - ({ id, name: label }))} - /> - ) -} - -const MenteeEnrolledInCourseField = (props) => { - return ( - - - {courseIdToLabelMap[props.record.mentee_currentlyEnrolledInCourse]} - - - ) -} -const MenteeEnrolledInCourseInput = (props) => { - const courses = coursesByLocation[props.record.rediLocation] - return ( - ({ id, name: label }))} - /> - ) -} - -const FullName = ({ record, sourcePrefix }) => { - return ( - - {get(record, `${sourcePrefix}firstName`)}{' '} - {get(record, `${sourcePrefix}lastName`)} - - ) -} -FullName.defaultProps = { - sourcePrefix: '', - label: 'Full name', -} - -const RedMatchList = (props) => ( - } - filters={} - exporter={redMatchesCsvExporter} - > - - - - - - - - - - - - - - -) -const RedMatchListFilters = (props) => ( - - - ({ id, name: label }))} - /> - -) -const RedMatchShow = (props) => ( - - - - - - - - - - - - - - - - - - -

Information about a mentor declining the mentorship

- - - - - - -
-
-) -const RedMatchShow_RelatedMentoringSessions = ({ - record: { mentorId, menteeId }, -}) => { - const [mentoringSessions, setMentoringSessions] = React.useState([]) - useEffect(() => { - dataProvider('GET_LIST', 'redMentoringSessions', { - pagination: { page: 1, perPage: 0 }, - sort: { field: 'date', order: 'ASC' }, - filter: { mentorId, menteeId }, - }).then(({ data }) => setMentoringSessions(data)) - }, []) - const totalDuration = mentoringSessions.reduce( - (acc, curr) => acc + curr.minuteDuration, - 0 - ) - if (mentoringSessions && mentoringSessions.length === 0) { - return

NO mentoring sessions registerd yet.

- } - return ( - mentoringSessions && - mentoringSessions.length > 0 && ( - <> -

Mentoring sessions registered

-
- - - - # - Date - Duration in minutes - - - - {mentoringSessions.map((row, index) => ( - - {index + 1} - - {new Date(row.date).toLocaleDateString('de-DE')} - - {row.minuteDuration} - - ))} - - - - Total - - - {totalDuration} - - - -
-
- - ) - ) -} -const RedMatchCreate = (props) => ( - - - - - `${op.firstName} ${op.lastName}`} - /> - - - `${op.firstName} ${op.lastName}`} - /> - - - - - -) -const RedMatchEdit = (props) => ( - - - - - `${op.firstName} ${op.lastName}`} - /> - - - `${op.firstName} ${op.lastName}`} - /> - - - - - - -

Information about a mentor declining the mentorship

- - - - -
-
-) - -const exporter = async (mentoringSessions, fetchRelatedRecords) => { - const mentors = await fetchRelatedRecords( - mentoringSessions, - 'mentorId', - 'redProfiles' - ) - const mentees = await fetchRelatedRecords( - mentoringSessions, - 'menteeId', - 'redProfiles' - ) - const data = mentoringSessions.map((x) => { - const mentor = mentors[x.mentorId] - const mentee = mentees[x.menteeId] - if (mentor) { - x.mentorName = `${mentor.firstName} ${mentor.lastName}` - } - if (mentee) { - x.menteeName = `${mentee.firstName} ${mentee.lastName}` - } - return x - }) - const csv = convertToCSV({ - data, - fields: [ - 'id', - 'date', - 'minuteDuration', - 'mentorName', - 'menteeName', - 'createdAt', - 'updatedAt', - ], - }) - downloadCSV(csv, 'yalla') -} - -const RedMentoringSessionList = (props) => ( - } - aside={} - filters={} - > - - - - - - - - - - - - - - -) -const RedMentoringSessionListFilters = (props) => ( - - ({ id, name: label }))} - /> - -) -const RedMentoringSessionListAside = () => { - const [fromDate, setFromDate] = React.useState(null) - const [toDate, setToDate] = React.useState(null) - const [rediLocation, setRediLocation] = React.useState(undefined) - const [loadState, setLoadState] = React.useState('pending') - const [result, setResult] = React.useState(null) - const [step, setStep] = React.useState(0) - const increaseStep = () => setStep((step) => step + 1) - - const picker = (getter, setter, label) => ( - - ) - - const valid = fromDate && toDate && toDate > fromDate - const doLoad = React.useCallback(() => - (async () => { - console.log('hello') - if (valid) { - setLoadState('loading') - setStep(0) - const sessions = await dataProvider( - 'GET_LIST', - 'redMentoringSessions', - { - pagination: { page: 1, perPage: 0 }, - sort: {}, - filter: { date: { gte: fromDate, lte: toDate }, rediLocation }, - } - ) - setLoadState('success') - setResult( - sessions.data.reduce((acc, curr) => acc + curr.minuteDuration, 0) - ) - } - })() - ) - - return ( -
- Isabelle Calculator - - - {picker(fromDate, setFromDate, 'From date')} - {picker(toDate, setToDate, 'To date')} - - - City - - -
- -
-
- {loadState === 'success' && step < 10 && ( - - )} -
- {step === 10 && ( - - Total: {result} minutes! That's {Math.floor(result / 60)} hours and{' '} - {result % 60} minutes - - )} -
- ) -} -const RedMentoringSessionShow = (props) => ( - - - - - - - - - - - - - - - -) - -const RedMentoringSessionCreate = (props) => ( - - - ({ id, name: label }))} - /> - - `${op.firstName} ${op.lastName}`} - /> - - - `${op.firstName} ${op.lastName}`} - /> - - - ({ - id: duration, - name: duration, - }))} - /> - - -) -const RedMentoringSessionEdit = (props) => ( - - - - `${op.firstName} ${op.lastName}`} - /> - - - `${op.firstName} ${op.lastName}`} - /> - - - ({ - id: duration, - name: duration, - }))} - /> - - -) - -const TpJobseekerProfileList = (props) => { - return ( - <> - } - pagination={} - > - }> - - - - - - {/* */} - - -

- A quick note regard state: -

-
    -
  1. - drafting-profile: the very first state. The jobseeker - has just signed up and his drafting their profile. -
  2. -
  3. - submitted-for-review: the jobseeker has provided at - least as much information as Talent Pool requires. Their profile has - been submitted to ReDI for review. Click Show > Edit to find two - buttons to Approve/Decline their profile. -
  4. -
  5. - profile-approved: the jobseeker's profile is approved -
  6. -
- - ) -} - -const TpJobseekerProfileListExpandPane = (props) => { - return ( - - - - - - - - ) -} - -const TpJobseekerProfileListFilters = (props) => ( - - - ({ - id: val, - name: val, - }))} - /> - - -) - -function tpJobseekerProfileListExporter(profiles, fetchRelatedRecords) { - const data = profiles.map((profile) => { - let { hrSummit2021JobFairCompanyJobPreferences } = profile - hrSummit2021JobFairCompanyJobPreferences = - hrSummit2021JobFairCompanyJobPreferences?.map( - ({ jobPosition, jobId, companyName }) => { - return `${jobPosition}${ - jobId ? ` (${jobId})` : '' - } --- ${companyName}` - } - ) - delete profile.hrSummit2021JobFairCompanyJobPreferences - - const { - firstName, - lastName, - contactEmail, - createdAt, - state, - jobseeker_currentlyEnrolledInCourse, - currentlyEnrolledInCourse, - loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName, - updatedAt, - lastLoginDateTime, - postalMailingAddress, - genderPronouns, - } = profile - - return { - firstName, - lastName, - contactEmail, - createdAt, - state, - jobseeker_currentlyEnrolledInCourse, - currentlyEnrolledInCourse, - loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName, - updatedAt, - lastLoginDateTime, - postalMailingAddress, - genderPronouns, - jobPreference1: hrSummit2021JobFairCompanyJobPreferences?.[0], - jobPreference2: hrSummit2021JobFairCompanyJobPreferences?.[1], - jobPreference3: hrSummit2021JobFairCompanyJobPreferences?.[2], - jobPreference4: hrSummit2021JobFairCompanyJobPreferences?.[3], - } - }) - - const csv = convertToCSV( - data - // { - // fields: [ - // 'id', - // 'firstName', - // 'lastName', - // 'contactEmail', - // 'hrSummit2021JobFairCompanyJobPreferences', - // 'createdAt', - // 'updatedAt', - // ], - // } - ) - downloadCSV(csv, 'yalla') -} - -const TpJobseekerProfileShow = (props) => ( - - - - - - - - - - - - - - - record?.desiredPositions?.join(', ')} - /> - - - - - - - - - - - - - - - - - - {/* */} - - - - record?.topSkills?.join(', ')} - /> - - - - - - - - - record?.startDateMonth - ? parseInt(record.startDateMonth) + 1 - : null - } - /> - - - record?.startDateMonth - ? parseInt(record.endDateMonth) + 1 - : null - } - /> - - - - - - - - - - - - - - record?.startDateMonth - ? parseInt(record.startDateMonth) + 1 - : null - } - /> - - - record?.startDateMonth - ? parseInt(record.endDateMonth) + 1 - : null - } - /> - - - - - {/* */} - - - - - - - - - - - - - - - - -

Record information

- - - -
- - - -
-
-
-) - -const TpJobseekerProfileEdit = (props) => ( - // }> - }> - - - - - - {/* */} - - - - - - - record?.desiredPositions?.join(', ')} - /> - - - - - - - - - - - - - - - - - - record?.desiredEmploymentType?.join(', ')} - /> - - - - record?.topSkills?.join(', ')} - /> - - - - - - - record?.startDateMonth - ? parseInt(record.startDateMonth) + 1 - : null - } - /> - - - record?.startDateMonth - ? parseInt(record.endDateMonth) + 1 - : null - } - /> - - - - - - - - - - - - record?.startDateMonth - ? parseInt(record.startDateMonth) + 1 - : null - } - /> - - - record?.startDateMonth - ? parseInt(record.endDateMonth) + 1 - : null - } - /> - - - - - {/* */} - - - - - - - - - - - - - - - - -

Record information

- - - -
- - - -
-
-) - -const TpJobseekerProfileEditActions = (props) => { - if (props?.data?.state !== 'submitted-for-review') return null - - return ( - - User is pending. Please or - - - ) -} - -const TpCompanyProfileEditActions = (props) => { - if (props?.data?.state === 'profile-approved') return null - - return ( - - Company profile needs to be approved before becoming active. Please{' '} - - - ) -} - -const TpCompanyProfileList = (props) => { - return ( - <> - } - filters={} - > - - - - - - - - - - -

- A quick note regard state: -

-
    -
  1. - drafting-profile: the very first state. The company - has just signed up and his drafting their profile. -
  2. -
  3. - submitted-for-review: the company has provided at - least as much information as Talent Pool requires. Their profile has - been submitted to ReDI for review. Click Show > Edit to find two - buttons to Approve/Decline their profile. -
  4. -
  5. - profile-approved: the company's profile is approved -
  6. -
- - ) -} - -const TpCompanyProfileListFilters = (props) => ( - - - ({ - id: val, - name: val, - }))} - /> - -) - -const ConditionalTpCompanyProfileHowDidHearAboutRediOtherTextFieldShow = ( - props -) => { - return props.record?.howDidHearAboutRediKey === 'other' && - props.record?.howDidHearAboutRediOtherText ? ( - - - - ) : null -} - -const ConditionalTpCompanyProfileHowDidHearAboutRediOtherTextFieldEdit = ( - props -) => { - return props.record?.howDidHearAboutRediKey === 'other' && - props.record?.howDidHearAboutRediOtherText ? ( - - ) : null -} - -const TpCompanyProfileShow = (props) => ( - - - - - - - - - - - - - - - - - - howDidHearAboutRediOptions[record.howDidHearAboutRediKey] - } - /> - - - - - - - - record?.idealTechnicalSkills?.join(', ')} - /> - record?.relatesToPositions?.join(', ')} - /> - - - - - - - - - - - - - - - - - - {/* - - */} - - - - - - - -) - -const TpCompanyProfileEdit = (props) => ( - }> - - - - - - - - - - - - - - - ({ id, name }) - )} - /> - - - - - - - - - record?.idealTechnicalSkills?.join(', ')} - /> - record?.relatesToPositions?.join(', ')} - /> - - - - - - - - - - - - - - - - - - - - - -) - -const TpJobListingListFilters = (props) => ( - - - -) - -const TpJobListingList = (props) => { - return ( - } - filters={} - exporter={tpJobListingListExporter} - > - - - - - - - - - - - - ) -} - -function tpJobListingListExporter(jobListings, fetchRelatedRecords) { - const data = jobListings.map((job) => { - const { - title, - location, - tpCompanyProfile: { companyName }, - employmentType, - languageRequirements, - desiredExperience, - salaryRange, - } = job - - return { - title, - location, - companyName, - employmentType, - languageRequirements, - desiredExperience, - salaryRange, - } - }) - - const csv = convertToCSV( - data - // { - // fields: [ - // 'id', - // 'firstName', - // 'lastName', - // 'contactEmail', - // 'hrSummit2021JobFairCompanyJobPreferences', - // 'createdAt', - // 'updatedAt', - // ], - // } - ) - downloadCSV(csv, 'Are you ReDI? Yalla habibi') -} - -const TpJobListingShow = (props) => ( - - - - - - - - - - - record?.idealTechnicalSkills?.join(', ')} - /> - record?.relatesToPositions?.join(', ')} - /> - - - - - - -) - -const TpJobListingEdit = (props) => ( - - - - - - - - - - - record?.idealTechnicalSkills?.join(', ')} - /> - record?.relatesToPositions?.join(', ')} - /> - - - - - - -) - -const TpJobFair2021InterviewMatchList = (props) => { - return ( - } - exporter={tpJobFair2021InterviewMatchListExporter} - > - - - - - - - - - - - - - - - - ) -} - -function tpJobFair2021InterviewMatchListExporter(matches, fetchRelatedRecords) { - const data = matches.map((match) => { - const { - company: { - companyName, - location: companyLocation, - firstName: companyPersonFirstName, - lastName: companyPersonLastName, - contactEmail: companyPersonContactEmail, - } = {}, - interviewee: { - currentlyEnrolledInCourse: intervieweeCurrentRediCourse, - firstName: intervieweeFirstName, - lastName: intervieweeLastName, - contactEmail: intervieweeContactEmail, - } = {}, - } = match - - return { - companyName, - companyLocation, - companyPersonFirstName, - companyPersonLastName, - companyPersonContactEmail, - intervieweeFirstName, - intervieweeLastName, - intervieweeContactEmail, - intervieweeCurrentRediCourse, - } - }) - - const csv = convertToCSV( - data - // { - // fields: [ - // 'id', - // 'firstName', - // 'lastName', - // 'contactEmail', - // 'hrSummit2021JobFairCompanyJobPreferences', - // 'createdAt', - // 'updatedAt', - // ], - // } - ) - downloadCSV(csv, 'Company-interviewee matches') -} - -const TpJobFair2021InterviewMatchShow = (props) => ( - - - - - - - - - - - - - -) - -const TpJobFair2021InterviewMatchCreate = (props) => ( - - - - `${op.firstName} ${op.lastName}`} - /> - - - `${op.companyName}`} /> - - - { - if (!op.tpCompanyProfile || !op.tpCompanyProfile.companyName) { - console.log(op) - } - return `${op.tpCompanyProfile.companyName} --- ${op.title}` - }} - /> - - - -) - -const TpJobFair2021InterviewMatchEdit = (props) => ( - - - - `${op.firstName} ${op.lastName}`} - /> - - - `${op.companyName}`} /> - - - - `${op.tpCompanyProfile.companyName} --- ${op.title}` - } - /> - - - -) - -const buildDataProvider = (normalDataProvider) => (verb, resource, params) => { - if (verb === 'GET_LIST' && resource === 'redProfiles') { - if (params.filter) { - const filter = params.filter - const q = filter.q - delete filter.q - const newFilter = { and: [filter] } - if (q) { - const andConditions = q.split(' ').map((word) => ({ - loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName: { - like: word, - options: 'i', - }, - })) - newFilter.and = [...newFilter.and, ...andConditions] - } - params.filter = newFilter - } - } - if (verb === 'GET_LIST' && resource === 'tpJobseekerProfiles') { - if (params.filter) { - const filter = params.filter - const q = filter.q - delete filter.q - const newFilter = { and: [filter] } - if (q) { - const andConditions = q.split(' ').map((word) => ({ - loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName: { - like: word, - options: 'i', - }, - })) - newFilter.and = [...newFilter.and, ...andConditions] - } - params.filter = newFilter - } - } - if (verb === 'GET_LIST' && resource === 'tpCompanyProfiles') { - if (params.filter) { - const filter = params.filter - const q = filter.q - delete filter.q - const newFilter = { and: [filter] } - if (q) { - const andConditions = q.split(' ').map((word) => ({ - companyName: { - like: word, - options: 'i', - }, - })) - newFilter.and = [...newFilter.and, ...andConditions] - } - params.filter = newFilter - } - } - return normalDataProvider(verb, resource, params) -} - -const dataProvider = buildDataProvider(loopbackClient(API_URL)) - -function App() { - return ( -
- - - - - - - - - -
- ) -} - -export default App diff --git a/apps/admin-panel/src/App.tsx b/apps/admin-panel/src/App.tsx new file mode 100644 index 000000000..a81dbb503 --- /dev/null +++ b/apps/admin-panel/src/App.tsx @@ -0,0 +1,2390 @@ +import React, { useCallback, useEffect, useState } from 'react' +import { get, mapValues, keyBy, groupBy } from 'lodash' +import { + Admin, + Resource, + List, + Tab, + Create, + Pagination, + Filter, + Datagrid, + TabbedForm, + FunctionField, + TabbedShowLayout, + TextField, + ReferenceInput, + AutocompleteInput, + DateField, + TextInput, + BooleanInput, + NullableBooleanInput, + NumberField, + FormTab, + NumberInput, + Show, + ShowButton, + LongTextInput, + CardActions, + DateInput, + EditButton, + SelectInput, + Edit, + SimpleForm, + ArrayField, + BooleanField, + SimpleShowLayout, + SelectArrayInput, + downloadCSV, + ReferenceField, + Labeled, + ReferenceManyField, + SearchInput, +} from 'react-admin' +import classNames from 'classnames' +import { unparse as convertToCSV } from 'papaparse/papaparse.min' +import { createStyles, withStyles } from '@material-ui/core' +import { Person as PersonIcon } from '@material-ui/icons' +import Button from '@material-ui/core/Button' +import Table from '@material-ui/core/Table' +import TableBody from '@material-ui/core/TableBody' +import TableCell from '@material-ui/core/TableCell' +import TableHead from '@material-ui/core/TableHead' +import TableRow from '@material-ui/core/TableRow' +import FormControl from '@material-ui/core/FormControl' +import InputLabel from '@material-ui/core/InputLabel' +import Select from '@material-ui/core/Select' +import MenuItem from '@material-ui/core/MenuItem' +import Typography from '@material-ui/core/Typography' +import Card from '@material-ui/core/Card' +import CardContent from '@material-ui/core/CardContent' +import DateFnsUtils from '@date-io/date-fns' + +import { + MuiPickersUtilsProvider, + KeyboardDatePicker, +} from '@material-ui/pickers' + +import { + REDI_LOCATION_NAMES, + LANGUAGES, + CATEGORY_GROUPS, + CATEGORIES, + COURSES, + GENDERS, + MENTORING_SESSION_DURATION_OPTIONS, + RED_MATCH_STATUSES, +} from '@talent-connect/shared-config' + +import { calculateAge } from '@talent-connect/shared-utils' + +import { howDidHearAboutRediOptions } from '@talent-connect/talent-pool/config' + +import loopbackClient, { authProvider } from './lib/react-admin-loopback/src' +import { ApproveButton } from './components/ApproveButton' +import { DeclineButton } from './components/DeclineButton' +import { TpJobSeekerProfileApproveButton } from './components/TpJobSeekerProfileApproveButton' +import { TpJobSeekerProfileDeclineButton } from './components/TpJobSeekerProfileDeclineButton' +import { TpCompanyProfileApproveButton } from './components/TpCompanyProfileApproveButton' + +import { API_URL } from './config' +import { + TpJobSeekerProfileState, + TpCompanyProfileState, + RedProfile, +} from '@talent-connect/shared-types' + +import { objectEntries, objectValues, mapOptionsObject } from '@talent-connect/typescript-utilities' + +import { redMatchesCsvExporter } from './utils/csvExport' + +/** REFERENCE DATA */ + +const rediLocations = mapOptionsObject(REDI_LOCATION_NAMES) + +const categoriesFlat = CATEGORIES.map((cat) => ({ + ...cat, + labelClean: cat.label, + label: `${cat.label} (${cat.group})`, +})) + +const coursesByLocation = groupBy(COURSES, 'location') +const coursesFlat = [ + ...coursesByLocation.berlin.map((cat) => + Object.assign(cat, { label: `Berlin: ${cat.label}` }) + ), + ...coursesByLocation.munich.map((cat) => + Object.assign(cat, { label: `Munich: ${cat.label}` }) + ), + ...coursesByLocation.nrw.map((cat) => + Object.assign(cat, { label: `NRW: ${cat.label}` }) + ), +] + +const categoriesIdToLabelCleanMap = + mapValues(keyBy(categoriesFlat, 'id'), 'labelClean') + +const categoriesIdToGroupMap = + mapValues(keyBy(categoriesFlat, 'id'), 'group') + +const genders = [ + ...objectEntries(GENDERS).map((id, name) => ({ id, name })), + { id: '', name: 'Prefers not to answer' }, +] + +const languages = LANGUAGES.map((lang) => ({ id: lang, name: lang })) + +const courseIdToLabelMap = mapValues(keyBy(coursesFlat, 'id'), 'label') +const AWS_PROFILE_AVATARS_BUCKET_BASE_URL = + 'https://s3-eu-west-1.amazonaws.com/redi-connect-profile-avatars/' + +export const formRedMatchStatuses = objectEntries(RED_MATCH_STATUSES) + .map(([key, value]) => ({ id: key, name: value })) + +/** START OF SHARED STUFF */ + +interface RecordProps { + addLabel?: boolean; + label?: string +} + +function RecordCreatedAt ({ addLabel = true, label = 'Record created at' }: RecordProps) { + return ( + + ); +} + +function RecordUpdatedAt ({ addLabel = true, label = 'Record updated at' }: RecordProps) { + return ( + + ) +} + +function LanguageList ({ data }: { data?: Record }) { + return {data && Object.values(data).join(', ')} +} + +function CategoryList ({ data }) { + const categoriesGrouped = groupBy( + data, + (catId) => categoriesIdToGroupMap[catId] + ) + return ( + <> + {Object.keys(categoriesGrouped).map((groupId, index) => ( + + + {CATEGORY_GROUPS[groupId]}:{' '} + {categoriesGrouped[groupId] + .map((catId) => categoriesIdToLabelCleanMap[catId]) + .join(', ')} + +
+
+ ))} + + ) +} + +const styles = createStyles({ + avatarImage: { + width: '500px', + height: '500px', + backgroundSize: 'cover', + backgroundPosition: 'center center', + }, +}) + +const Avatar = withStyles(styles)(({ record, className, classes, style }) => ( + <> + {!record && ( + + )} + {record?.profileAvatarImageS3Key && ( +
+ )} + +)) + +/** END OF SHARED STUFF */ +function AllModelsPagination (props) { + return ( + + ); +} + +function RedProfileList (props) { + return ( + } + pagination={} + aside={} + exporter={redProfileListExporter} + > + }> + + + + + ; + + + + + + + + + + + + + ) +} + +function redProfileListExporter(profiles) { + const properties = [ + 'id', + 'userType', + 'rediLocation', + 'firstName', + 'lastName', + 'gender', + 'age', + 'birthDate', + 'userActivated', + 'userActivatedAt', + 'mentor_occupation', + 'mentor_workPlace', + 'expectations', + 'mentor_ifTypeForm_submittedAt', + 'mentee_ifTypeForm_preferredMentorSex', + 'mentee_currentCategory', + 'mentee_occupationCategoryId', + 'mentee_occupationJob_placeOfEmployment', + 'mentee_occupationJob_position', + 'mentee_occupationStudent_studyPlace', + 'mentee_occupationStudent_studyName', + 'mentee_occupationLookingForJob_what', + 'mentee_occupationOther_description', + 'mentee_highestEducationLevel', + 'mentee_currentlyEnrolledInCourse', + 'profileAvatarImageS3Key', + 'languages', + 'otherLanguages', + 'personalDescription', + 'contactEmail', + 'linkedInProfileUrl', + 'githubProfileUrl', + 'slackUsername', + 'telephoneNumber', + 'categories', + 'favouritedRedProfileIds', + 'optOutOfMenteesFromOtherRediLocation', + 'signupSource', + 'currentApplicantCount', + 'menteeCountCapacity', + 'currentMenteeCount', + 'currentFreeMenteeSpots', + 'ifUserIsMentee_hasActiveMentor', + 'ifUserIsMentee_activeMentor', + 'ifTypeForm_additionalComments', + 'createdAt', + 'updatedAt', + 'gaveGdprConsentAt', + 'administratorInternalComment', + ] + + const data = profiles.map((profile) => + objectEntries(properties.map((prop) => [prop, profile[prop]]))) + + const csv = convertToCSV(data) + downloadCSV(csv, 'yalla') +} + +function FreeMenteeSpotsPerLocationAside() { + const [mentorsList, setMentorsList] = useState([]) + + useEffect(() => { + dataProvider('GET_LIST', 'redProfiles', { + pagination: { page: 1, perPage: 0 }, + sort: {}, + filter: { userType: 'mentor' }, + }).then(({ data }) => setMentorsList(data)) + }, []) + + function getFreeSpotsCount (location) { + return mentorsList + .filter(({ rediLocation }) => rediLocation === location) + .filter(({ userActivated }) => userActivated) + .reduce((acc, curr) => acc + curr.currentFreeMenteeSpots, 0) + } + + const totalFreeMenteeSpotsBerlin = getFreeSpotsCount('berlin') + const totalFreeMenteeSpotsMunich = getFreeSpotsCount('munich') + const totalFreeMenteeSpotsNRW = getFreeSpotsCount('nrw') + + return ( +
+ + + Free Mentee Spots Per Location + + Berlin: {totalFreeMenteeSpotsBerlin} mentoring spots available + + + Munich: {totalFreeMenteeSpotsMunich} mentoring spots available + + + NRW: {totalFreeMenteeSpotsNRW} mentoring spots available + + + +
+ ) +} + +function RedProfileListExpandPane (props) { + return ( + + + + + + + + + + + + ) +} + +function RedProfileListFilters (props) { + return ( + + + ({ id, name: label }))} + /> + + ({ id, name: label }))} + /> + ({ id, name: label }))} + /> + + + ); +} + +function userTypeToEmoji({ userType }: RedProfile) { + const emoji = { + mentor: '🎁 Mentor', + mentee: 'πŸ’Ž Mentee', + 'public-sign-up-mentor-pending-review': 'πŸ’£ Mentor (pending review)', + 'public-sign-up-mentee-pending-review': '🧨 Mentee (pending review)', + 'public-sign-up-mentor-rejected': '❌🎁 Mentor (rejected)', + 'public-sign-up-mentee-rejected': 'βŒπŸ’ŽMentee (rejected)', + }[userType] + return emoji ?? userType +} + +function RedProfileShow (props) { + return ( + + + + + + + + + + + + calculateAge(person.birthDate)} + /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Mentor-specific fields:

+ + + +

Mentee-specific fields:

+ + + + + + + + + +

Record information

+ + + +

+ Typeform information (for mentors/mentees originally signed up via + typeform) +

+ + + +
+ + + +
+
+
+ ); +} + +function RedProfileEditActions(props) { + const userType = props.data?.userType + if ( + ![ + 'public-sign-up-mentor-pending-review', + 'public-sign-up-mentee-pending-review', + ].includes(userType) + ) return null + return ( + + User is pending. Please + or + + + ) +} + +function RedProfileEdit (props) { + return ( + }> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +function CategoriesInput (props) { + const categories = categoriesFlat + return ( + ({ id, name: label }))} + /> + ) +} + +function MenteeEnrolledInCourseField (props: { record: any }) { + return ( + + + {courseIdToLabelMap[props.record.mentee_currentlyEnrolledInCourse]} + + + ) +} + +function MenteeEnrolledInCourseInput (props: { record: any }) { + const courses = coursesByLocation[props.record.rediLocation] + return ( + ({ id, name: label }))} + /> + ) +} + +interface FullNameProps { + sourcePrefix: 'mentee' | 'mentor' | ''; + record: Record // TODO +} + +function FullName ({ record, sourcePrefix: source = '' }: FullNameProps) { + return ( + + {get(record, `${source}firstName`)}{' '} + {get(record, `${source}lastName`)} + + ) +} + +function RedMatchList (props) { + return ( + } + filters={} + exporter={redMatchesCsvExporter} + > + + + + + + + + + + + + + + + ); +} + +function RedMatchListFilters (props) { + return ( + + + ({ id, name: label }))} + /> + + ); +} + +function RedMatchShow (props) { + return ( + + + + + + + + + + + + + + + + + + +

Information about a mentor declining the mentorship

+ + + + + + +
+
+ ); +} + +function RedMatchShow_RelatedMentoringSessions ({ // TODO: type + record: { mentorId, menteeId }, +}: { record: any }) { + const [mentoringSessions, setMentoringSessions] = useState([]) + useEffect(() => { + dataProvider('GET_LIST', 'redMentoringSessions', { + pagination: { page: 1, perPage: 0 }, + sort: { field: 'date', order: 'ASC' }, + filter: { mentorId, menteeId }, + }).then(({ data }) => setMentoringSessions(data)) + }, []) + const totalDuration = mentoringSessions.reduce( + (acc, curr) => acc + curr.minuteDuration, + 0 + ) + if (!mentoringSessions?.length) { + return

NO mentoring sessions registerd yet.

+ } + + return ( + mentoringSessions?.length && ( + <> +

Mentoring sessions registered

+
+ + + + # + Date + Duration in minutes + + + + {mentoringSessions.map((row, index) => ( + + {index + 1} + + {new Date(row.date).toLocaleDateString('de-DE')} + + {row.minuteDuration} + + ))} + + + + Total + + + {totalDuration} + + + +
+
+ + ) + ) +} + +function RedMatchCreate (props) { + return ( + + + + + `${op.firstName} ${op.lastName}`} + /> + + + `${op.firstName} ${op.lastName}`} + /> + + + + + + ); +} + +function RedMatchEdit (props) { + return ( + + + + + `${op.firstName} ${op.lastName}`} + /> + + + `${op.firstName} ${op.lastName}`} + /> + + + + + + +

Information about a mentor declining the mentorship

+ + + + +
+
+ ); +} + +const exporter = async (mentoringSessions, fetchRelatedRecords) => { + const mentors = await fetchRelatedRecords( + mentoringSessions, + 'mentorId', + 'redProfiles' + ) + const mentees = await fetchRelatedRecords( + mentoringSessions, + 'menteeId', + 'redProfiles' + ) + const data = mentoringSessions.map((session) => { + const mentor = mentors[session.mentorId] + const mentee = mentees[session.menteeId] + if (mentor) session.mentorName = `${mentor.firstName} ${mentor.lastName}` + if (mentee) session.menteeName = `${mentee.firstName} ${mentee.lastName}` + return session + }) + const csv = convertToCSV({ + data, + fields: [ + 'id', + 'date', + 'minuteDuration', + 'mentorName', + 'menteeName', + 'createdAt', + 'updatedAt', + ], + }) + downloadCSV(csv, 'yalla') +} + +function RedMentoringSessionList (props) { + return ( + } + aside={} + filters={} + > + + + + + + + + + + + + + + + ); +} + +function RedMentoringSessionListFilters (props) { + return ( + + ({ id: value, name: label }))} + /> + + ); +} + +function RedMentoringSessionListAside () { + const [fromDate, setFromDate] = useState(null) + const [toDate, setToDate] = useState(null) + const [rediLocation, setRediLocation] = useState(null) + const [loadState, setLoadState] = useState('pending') + const [result, setResult] = useState(null) + const [step, setStep] = useState(0) + const increaseStep = () => setStep((step) => step + 1) + + const picker = (getter, setter, label) => ( + + ) + + const valid = fromDate && toDate && toDate > fromDate + const doLoad = useCallback(() => (async () => { + if (!valid) return + + setLoadState('loading') + setStep(0) + const { data } = await dataProvider( + 'GET_LIST', + 'redMentoringSessions', + { + pagination: { page: 1, perPage: 0 }, + sort: {}, + filter: { date: { gte: fromDate, lte: toDate }, rediLocation }, + } + ) + setLoadState('success') + setResult(data.reduce((acc, curr) => acc + curr.minuteDuration, 0)) + })(), + [] + ) + + return ( +
+ Isabelle Calculator + + + {picker(fromDate, setFromDate, 'From date')} + {picker(toDate, setToDate, 'To date')} + + + City + + +
+ +
+
+ {loadState === 'success' && step < 10 && ( + + )} +
+ {step === 10 && ( + + Total: {result} minutes! That's {Math.floor(result / 60)} hours and{' '} + {result % 60} minutes + + )} +
+ ) +} + +function RedMentoringSessionShow (props) { + return ( + + + + + + + + + + + + + + + + ); +} + + +function RedMentoringSessionCreate (props) { + return ( + + + ({ id: value, name: label }))} + /> + + `${op.firstName} ${op.lastName}`} + /> + + + `${op.firstName} ${op.lastName}`} + /> + + + ({ + id: duration, + name: duration, + }))} + /> + + + ); +} + +function RedMentoringSessionEdit (props) { + return ( + + + + `${op.firstName} ${op.lastName}`} + /> + + + `${op.firstName} ${op.lastName}`} + /> + + + ({ + id: duration, + name: duration, + }))} + /> + + + ); +} + +function TpJobSeekerProfileList (props) { + return ( + <> + } + pagination={} + > + }> + + + + + + {/* */} + + +

A quick note regard state:

+
    +
  1. + drafting-profile: the very first state. The jobSeeker + has just signed up and his drafting their profile. +
  2. +
  3. + submitted-for-review: the jobSeeker has provided at + least as much information as Talent Pool requires. Their profile has + been submitted to ReDI for review. Click Show > Edit to find two + buttons to Approve/Decline their profile. +
  4. +
  5. + profile-approved: the jobSeeker's profile is approved +
  6. +
+ + ) +} + +function TpJobSeekerProfileListExpandPane (props) { + return ( + + + + + + + + ) +} + +function TpJobSeekerProfileListFilters (props) { + return ( + + + ({ id: val, name: val }))} + /> + + + ); +} + +function tpJobSeekerProfileListExporter(profiles, fetchRelatedRecords) { + const data = profiles.map((profile) => { + let { hrSummit2021JobFairCompanyJobPreferences } = profile + hrSummit2021JobFairCompanyJobPreferences = + hrSummit2021JobFairCompanyJobPreferences?.map(({ jobPosition, jobId, companyName }) => { + return `${jobPosition}${jobId ? ` (${jobId})` : ''} --- ${companyName}` + } + ) + delete profile.hrSummit2021JobFairCompanyJobPreferences //TODO avoid delete + + const { + firstName, + lastName, + contactEmail, + createdAt, + state, + jobSeeker_currentlyEnrolledInCourse, + currentlyEnrolledInCourse, + loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName, + updatedAt, + lastLoginDateTime, + postalMailingAddress, + genderPronouns, + } = profile + + return { + firstName, + lastName, + contactEmail, + createdAt, + state, + jobSeeker_currentlyEnrolledInCourse, + currentlyEnrolledInCourse, + loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName, + updatedAt, + lastLoginDateTime, + postalMailingAddress, + genderPronouns, + jobPreference1: hrSummit2021JobFairCompanyJobPreferences?.[0], + jobPreference2: hrSummit2021JobFairCompanyJobPreferences?.[1], + jobPreference3: hrSummit2021JobFairCompanyJobPreferences?.[2], + jobPreference4: hrSummit2021JobFairCompanyJobPreferences?.[3], + } + }) + + const csv = convertToCSV( + data + // { + // fields: [ + // 'id', + // 'firstName', + // 'lastName', + // 'contactEmail', + // 'hrSummit2021JobFairCompanyJobPreferences', + // 'createdAt', + // 'updatedAt', + // ], + // } + ) + downloadCSV(csv, 'yalla') +} + +function TpJobSeekerProfileShow (props) { + return ( + + + + + + + + + + + + + + + record?.desiredPositions?.join(', ')} + /> + + + + + + + + + + + + + + + + + + {/* */} + + + + record?.topSkills?.join(', ')} + /> + + + + + + + + + record?.startDateMonth + ? parseInt(record.startDateMonth) + 1 + : null + } + /> + + + record?.startDateMonth + ? parseInt(record.endDateMonth) + 1 + : null + } + /> + + + + + + + + + + + + + + record?.startDateMonth + ? parseInt(record.startDateMonth) + 1 + : null + } + /> + + + record?.startDateMonth + ? parseInt(record.endDateMonth) + 1 + : null + } + /> + + + + + {/* */} + + + + + + + + + + + + + + + + +

Record information

+ + + +
+ + + +
+
+
+ ); +} + +function TpJobSeekerProfileEdit (props) { + return ( + // }> + }> + + + + + + {/* */} + + + + + + + record?.desiredPositions?.join(', ')} + /> + + + + + + + + + + + + + + + + + + record?.desiredEmploymentType?.join(', ')} + /> + + + + record?.topSkills?.join(', ')} + /> + + + + + + + record?.startDateMonth + ? parseInt(record.startDateMonth) + 1 + : null + } + /> + + + record?.startDateMonth + ? parseInt(record.endDateMonth) + 1 + : null + } + /> + + + + + + + + + + + + record?.startDateMonth + ? parseInt(record.startDateMonth) + 1 + : null + } + /> + + + record?.startDateMonth + ? parseInt(record.endDateMonth) + 1 + : null + } + /> + + + + + {/* */} + + + + + + + + + + + + + + + + +

Record information

+ + + +
+ + + +
+
+ ); +} + +function TpJobSeekerProfileEditActions (props: { data?: { state: string }}) { + if (props.data?.state !== 'submitted-for-review') return null + + return ( + + User is pending. Please or + + + ) +} + +function TpCompanyProfileEditActions (props: { data?: { state: string }}) { + if (props.data?.state === 'profile-approved') return null + + return ( + + Company profile needs to be approved before becoming active. Please{' '} + + + ) +} + +function TpCompanyProfileList (props) { + return ( + <> + } + filters={} + > + + + + + + + + + + +

+ A quick note regard state: +

+
    +
  1. + drafting-profile: the very first state. The company + has just signed up and his drafting their profile. +
  2. +
  3. + submitted-for-review: the company has provided at + least as much information as Talent Pool requires. Their profile has + been submitted to ReDI for review. Click Show > Edit to find two + buttons to Approve/Decline their profile. +
  4. +
  5. + profile-approved: the company's profile is approved +
  6. +
+ + ) +} + +function TpCompanyProfileListFilters (props) { + return ( + + + ({ id: val, name: val }))} + /> + + ); +} + +interface ProvProps { + record?: { + howDidHearAboutRediOtherText: string; + howDidHearAboutRediKey: string; + } +} + +function ConditionalTpCompanyProfileHowDidHearAboutRediOtherTextFieldShow (props: ProvProps) { // TODO: type props + const { record: { howDidHearAboutRediKey, howDidHearAboutRediOtherText } } = props; + return howDidHearAboutRediOtherText && howDidHearAboutRediKey === 'other' && ( + + + + ) +} + +function ConditionalTpCompanyProfileHowDidHearAboutRediOtherTextFieldEdit (props: ProvProps) { + const { record: { howDidHearAboutRediKey, howDidHearAboutRediOtherText } } = props; + return howDidHearAboutRediOtherText && howDidHearAboutRediKey === 'other' && ( + + ) +} + +function TpCompanyProfileShow (props) { + return ( + + + + + + + + + + + + + + + + + + howDidHearAboutRediOptions[record.howDidHearAboutRediKey] + } + /> + + + + + + + + record?.idealTechnicalSkills?.join(', ')} + /> + record?.relatesToPositions?.join(', ')} + /> + + + + + + + + + + + + + + + + + + {/* + + */} + + + + + + + + ); +} + + +function TpCompanyProfileEdit (props) { + return ( + }> + + + + + + + + + + + + + + + ({ id, name }))} + /> + + + + + + + + + record?.idealTechnicalSkills?.join(', ')} + /> + record?.relatesToPositions?.join(', ')} + /> + + + + + + + + + + + + + + + + + + + + + + ); +} + +function TpJobListingListFilters (props) { + return ( + + + + ); +} + +function TpJobListingList (props) { + return ( + } + filters={} + exporter={tpJobListingListExporter} + > + + + + + + + + + + + + ) +} + +function tpJobListingListExporter(jobListings, fetchRelatedRecords) { + const data = jobListings.map(({ + title, + location, + tpCompanyProfile: { companyName }, + employmentType, + languageRequirements, + desiredExperience, + salaryRange, + }) => { + return { + title, + location, + companyName, + employmentType, + languageRequirements, + desiredExperience, + salaryRange, + } + }) + + const csv = convertToCSV( + data + // { + // fields: [ + // 'id', + // 'firstName', + // 'lastName', + // 'contactEmail', + // 'hrSummit2021JobFairCompanyJobPreferences', + // 'createdAt', + // 'updatedAt', + // ], + // } + ) + downloadCSV(csv, 'Are you ReDI? Yalla habibi') +} + +function TpJobListingShow (props) { + return ( + + + + + + + + + + + record?.idealTechnicalSkills?.join(', ')} + /> + record?.relatesToPositions?.join(', ')} + /> + + + + + + + ); +} + +function TpJobListingEdit (props) { + return ( + + + + + + + + + + + record?.idealTechnicalSkills?.join(', ')} + /> + record?.relatesToPositions?.join(', ')} + /> + + + + + + + ); +} + +function TpJobFair2021InterviewMatchList (props) { + return ( + } + exporter={tpJobFair2021InterviewMatchListExporter} + > + + + + + + + + + + + + + + + + ) +} + +function tpJobFair2021InterviewMatchListExporter(matches, fetchRelatedRecords) { + const data = matches.map(({ company = {}, interviewee = {} }) => { + const { + companyName, + location: companyLocation, + firstName: companyPersonFirstName, + lastName: companyPersonLastName, + contactEmail: companyPersonContactEmail, + } = company + + const { + currentlyEnrolledInCourse: intervieweeCurrentRediCourse, + firstName: intervieweeFirstName, + lastName: intervieweeLastName, + contactEmail: intervieweeContactEmail, + } = interviewee + + return { + companyName, + companyLocation, + companyPersonFirstName, + companyPersonLastName, + companyPersonContactEmail, + intervieweeFirstName, + intervieweeLastName, + intervieweeContactEmail, + intervieweeCurrentRediCourse, + } + }) + + const csv = convertToCSV( + data + // { + // fields: [ + // 'id', + // 'firstName', + // 'lastName', + // 'contactEmail', + // 'hrSummit2021JobFairCompanyJobPreferences', + // 'createdAt', + // 'updatedAt', + // ], + // } + ) + downloadCSV(csv, 'Company-interviewee matches') +} + +function TpJobFair2021InterviewMatchShow (props) { + return ( + + + + + + + + + + + + + + ); +} + +function TpJobFair2021InterviewMatchCreate (props) { + return ( + + + + `${op.firstName} ${op.lastName}`} + /> + + + `${op.companyName}`} /> + + + { + if (!op.tpCompanyProfile || !op.tpCompanyProfile.companyName) { + console.log(op); + } + return `${op.tpCompanyProfile.companyName} --- ${op.title}`; + }} + /> + + + + ); +} + +function TpJobFair2021InterviewMatchEdit (props) { + return ( + + + + `${op.firstName} ${op.lastName}`} + /> + + + `${op.companyName}`} /> + + + + `${op.tpCompanyProfile.companyName} --- ${op.title}` + } + /> + + + + ); +} + +const buildDataProvider = (normalDataProvider) => (verb, resource, params) => { + if (params.filter && verb === 'GET_LIST') { + const filter = params.filter; + const q = filter.q; + delete filter.q; //TODO avoid delete + const newFilter = { and: [filter] }; + + if (q) { + if (resource === 'tpJobSeekerProfiles' || resource === 'redProfiles') { + const andConditions = q.split(' ').map((word) => ({ + loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName: { + like: word, + options: 'i', + }, + })) + newFilter.and = [...newFilter.and, ...andConditions] + } + if (resource === 'tpCompanyProfiles') { + const andConditions = q.split(' ').map((word) => ({ + companyName: { + like: word, + options: 'i', + }, + })) + newFilter.and = [...newFilter.and, ...andConditions] + } + } + params.filter = newFilter + } + return normalDataProvider(verb, resource, params) +} + +const dataProvider = buildDataProvider(loopbackClient(API_URL)) + +function App() { + return ( +
+ + + + + + + + + +
+ ) +} + +export default App diff --git a/apps/admin-panel/src/components/ApproveButton.jsx b/apps/admin-panel/src/components/ApproveButton.tsx similarity index 69% rename from apps/admin-panel/src/components/ApproveButton.jsx rename to apps/admin-panel/src/components/ApproveButton.tsx index beec671b8..f62039b72 100644 --- a/apps/admin-panel/src/components/ApproveButton.jsx +++ b/apps/admin-panel/src/components/ApproveButton.tsx @@ -1,5 +1,3 @@ -import React from 'react' -import Button from '@material-ui/core/Button' import { buildApproveOrRejectButton } from './component-factories/build-approve-or-decline-button' export const ApproveButton = buildApproveOrRejectButton('APPROVE') diff --git a/apps/admin-panel/src/components/ApproveRejectButton.jsx b/apps/admin-panel/src/components/ApproveRejectButton.tsx similarity index 54% rename from apps/admin-panel/src/components/ApproveRejectButton.jsx rename to apps/admin-panel/src/components/ApproveRejectButton.tsx index 11cf065ad..c9ec45a22 100644 --- a/apps/admin-panel/src/components/ApproveRejectButton.jsx +++ b/apps/admin-panel/src/components/ApproveRejectButton.tsx @@ -1,6 +1,5 @@ -import React from 'react' import Button from '@material-ui/core/Button' -export const ApproveRejectButton = () => { +export function ApproveRejectButton () { return } diff --git a/apps/admin-panel/src/components/DeclineButton.jsx b/apps/admin-panel/src/components/DeclineButton.tsx similarity index 69% rename from apps/admin-panel/src/components/DeclineButton.jsx rename to apps/admin-panel/src/components/DeclineButton.tsx index e20fc74e2..f7061c933 100644 --- a/apps/admin-panel/src/components/DeclineButton.jsx +++ b/apps/admin-panel/src/components/DeclineButton.tsx @@ -1,5 +1,3 @@ -import React from 'react' -import Button from '@material-ui/core/Button' import { buildApproveOrRejectButton } from './component-factories/build-approve-or-decline-button' export const DeclineButton = buildApproveOrRejectButton('DECLINE') diff --git a/apps/admin-panel/src/components/TpCompanyProfileApproveButton.jsx b/apps/admin-panel/src/components/TpCompanyProfileApproveButton.tsx similarity index 61% rename from apps/admin-panel/src/components/TpCompanyProfileApproveButton.jsx rename to apps/admin-panel/src/components/TpCompanyProfileApproveButton.tsx index d9f671dcb..f7e04e63e 100644 --- a/apps/admin-panel/src/components/TpCompanyProfileApproveButton.jsx +++ b/apps/admin-panel/src/components/TpCompanyProfileApproveButton.tsx @@ -1,10 +1,9 @@ /** * TODO: No need for this file at all. This is done to keep everything - * consistent among TpJobseeker and TpCompany so refactoring them together + * consistent among TpJobSeeker and TpCompany so refactoring them together * in the future is easier */ import { tpCompanyProfileBuildApproveButton } from './component-factories/tp-company-profile-build-approve-button' -export const TpCompanyProfileApproveButton = - tpCompanyProfileBuildApproveButton() +export const TpCompanyProfileApproveButton = tpCompanyProfileBuildApproveButton() diff --git a/apps/admin-panel/src/components/TpJobseekerProfileApproveButton.jsx b/apps/admin-panel/src/components/TpJobseekerProfileApproveButton.jsx deleted file mode 100644 index 2c19ba246..000000000 --- a/apps/admin-panel/src/components/TpJobseekerProfileApproveButton.jsx +++ /dev/null @@ -1,4 +0,0 @@ -import { tpJobseekerProfileBuildApproveOrRejectButton } from './component-factories/tp-jobseeker-profile-build-approve-or-decline-button' - -export const TpJobseekerProfileApproveButton = - tpJobseekerProfileBuildApproveOrRejectButton('APPROVE') diff --git a/apps/admin-panel/src/components/TpJobseekerProfileApproveButton.tsx b/apps/admin-panel/src/components/TpJobseekerProfileApproveButton.tsx new file mode 100644 index 000000000..6704d9afd --- /dev/null +++ b/apps/admin-panel/src/components/TpJobseekerProfileApproveButton.tsx @@ -0,0 +1,4 @@ +import { tpJobSeekerProfileBuildApproveOrRejectButton } from './component-factories/tp-jobSeeker-profile-build-approve-or-decline-button' + +export const TpJobSeekerProfileApproveButton = + tpJobSeekerProfileBuildApproveOrRejectButton('APPROVE') diff --git a/apps/admin-panel/src/components/TpJobseekerProfileDeclineButton.jsx b/apps/admin-panel/src/components/TpJobseekerProfileDeclineButton.jsx deleted file mode 100644 index 93b54779c..000000000 --- a/apps/admin-panel/src/components/TpJobseekerProfileDeclineButton.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react' -import Button from '@material-ui/core/Button' -import { tpJobseekerProfileBuildApproveOrRejectButton } from './component-factories/tp-jobseeker-profile-build-approve-or-decline-button' - -export const TpJobseekerProfileDeclineButton = tpJobseekerProfileBuildApproveOrRejectButton( - 'DECLINE' -) diff --git a/apps/admin-panel/src/components/TpJobseekerProfileDeclineButton.tsx b/apps/admin-panel/src/components/TpJobseekerProfileDeclineButton.tsx new file mode 100644 index 000000000..f399081a9 --- /dev/null +++ b/apps/admin-panel/src/components/TpJobseekerProfileDeclineButton.tsx @@ -0,0 +1,4 @@ +import { tpJobSeekerProfileBuildApproveOrRejectButton } from './component-factories/tp-jobSeeker-profile-build-approve-or-decline-button' + +export const TpJobSeekerProfileDeclineButton = + tpJobSeekerProfileBuildApproveOrRejectButton('DECLINE') diff --git a/apps/admin-panel/src/components/component-factories/build-approve-or-decline-button.jsx b/apps/admin-panel/src/components/component-factories/build-approve-or-decline-button.tsx similarity index 89% rename from apps/admin-panel/src/components/component-factories/build-approve-or-decline-button.jsx rename to apps/admin-panel/src/components/component-factories/build-approve-or-decline-button.tsx index 916b60def..687d82190 100644 --- a/apps/admin-panel/src/components/component-factories/build-approve-or-decline-button.jsx +++ b/apps/admin-panel/src/components/component-factories/build-approve-or-decline-button.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import { useCallback } from 'react' import Button from '@material-ui/core/Button' import { API_URL } from '../../config' @@ -11,7 +11,7 @@ import doApiRequest from '../../lib/react-admin-loopback/src/fetch' export const buildApproveOrRejectButton = (operationType) => { const ConfiguredButton = ({ data }) => { // On click, make a request to either approve or reject - const onClick = React.useCallback(() => { + const onClick = useCallback(() => { const sendRequest = async () => { const finalConfirmationPrompt = `Are you certain you want to ${operationTypeToLabelMap[operationType]} this user?` const shouldContinue = await showConfirmPrompt(finalConfirmationPrompt) @@ -24,12 +24,11 @@ export const buildApproveOrRejectButton = (operationType) => { method: 'post', } try { - const responseRaw = await doApiRequest(requestUrl, requestPayload) - const response = responseRaw.json + const { json: response } = await doApiRequest(requestUrl, requestPayload) alert(`User ${operationTypeToLabelMap[operationType]} completed`) window.location.reload() } catch (err) { - alert(`Error occured: ${err}`) + alert(`Error occurred: ${err}`) } } sendRequest() diff --git a/apps/admin-panel/src/components/component-factories/tp-company-profile-build-approve-button.jsx b/apps/admin-panel/src/components/component-factories/tp-company-profile-build-approve-button.tsx similarity index 84% rename from apps/admin-panel/src/components/component-factories/tp-company-profile-build-approve-button.jsx rename to apps/admin-panel/src/components/component-factories/tp-company-profile-build-approve-button.tsx index 26bf9b6af..3144cf13b 100644 --- a/apps/admin-panel/src/components/component-factories/tp-company-profile-build-approve-button.jsx +++ b/apps/admin-panel/src/components/component-factories/tp-company-profile-build-approve-button.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import { useCallback } from 'react' import Button from '@material-ui/core/Button' import { API_URL } from '../../config' @@ -11,18 +11,16 @@ import doApiRequest from '../../lib/react-admin-loopback/src/fetch' export const tpCompanyProfileBuildApproveButton = () => { const ConfiguredButton = ({ data }) => { // On click, make a request to approve - const onClick = React.useCallback(() => { + const onClick = useCallback(() => { const sendRequest = async () => { - const finalConfirmationPrompt = `Are you certain you want to aporove this user?` + const finalConfirmationPrompt = `Are you certain you want to approve this user?` const shouldContinue = await showConfirmPrompt(finalConfirmationPrompt) if (!shouldContinue) return const requestUrl = `${API_URL}/tpCompanyProfiles/pendingReviewDoAccept` const requestPayload = { - body: JSON.stringify({ - tpCompanyProfileId: data.id, - }), + body: JSON.stringify({ tpCompanyProfileId: data.id }), method: 'post', } @@ -33,7 +31,7 @@ export const tpCompanyProfileBuildApproveButton = () => { window.location.reload() } catch (err) { - alert(`Error occured: ${err}`) + alert(`Error occurred: ${err}`) } } diff --git a/apps/admin-panel/src/components/component-factories/tp-jobseeker-profile-build-approve-or-decline-button.jsx b/apps/admin-panel/src/components/component-factories/tp-jobseeker-profile-build-approve-or-decline-button.tsx similarity index 82% rename from apps/admin-panel/src/components/component-factories/tp-jobseeker-profile-build-approve-or-decline-button.jsx rename to apps/admin-panel/src/components/component-factories/tp-jobseeker-profile-build-approve-or-decline-button.tsx index fb4652741..d77a1e6f6 100644 --- a/apps/admin-panel/src/components/component-factories/tp-jobseeker-profile-build-approve-or-decline-button.jsx +++ b/apps/admin-panel/src/components/component-factories/tp-jobseeker-profile-build-approve-or-decline-button.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import { useCallback } from 'react' import Button from '@material-ui/core/Button' import { API_URL } from '../../config' @@ -8,10 +8,10 @@ import doApiRequest from '../../lib/react-admin-loopback/src/fetch' * Builds a button either approving or rejecting a RedProfile with review status * @param {string} buttonType */ -export const tpJobseekerProfileBuildApproveOrRejectButton = (operationType) => { +export const tpJobSeekerProfileBuildApproveOrRejectButton = (operationType) => { const ConfiguredButton = ({ data }) => { // On click, make a request to either approve or reject - const onClick = React.useCallback(() => { + const onClick = useCallback(() => { const sendRequest = async () => { const finalConfirmationPrompt = `Are you certain you want to ${operationTypeToLabelMap[operationType]} this user?` const shouldContinue = await showConfirmPrompt(finalConfirmationPrompt) @@ -19,7 +19,7 @@ export const tpJobseekerProfileBuildApproveOrRejectButton = (operationType) => { const requestUrl = operationUrl(operationType) const requestPayload = { body: JSON.stringify({ - tpJobseekerProfileId: data.id, + tpJobSeekerProfileId: data.id, }), method: 'post', } @@ -29,7 +29,7 @@ export const tpJobseekerProfileBuildApproveOrRejectButton = (operationType) => { alert(`User ${operationTypeToLabelMap[operationType]} completed`) window.location.reload() } catch (err) { - alert(`Error occured: ${err}`) + alert(`Error occurred: ${err}`) } } sendRequest() @@ -50,7 +50,7 @@ const showConfirmPrompt = (promptText) => const operationUrl = (operationType) => operationTypeValidOrThrow(operationType) && - `${API_URL}/tpJobseekerProfiles/${operationTypeToRedProfileCollectionMethodMap[operationType]}` + `${API_URL}/tpJobSeekerProfiles/${operationTypeToRedProfileCollectionMethodMap[operationType]}` const operationTypeToRedProfileCollectionMethodMap = { APPROVE: 'pendingReviewDoAccept', @@ -63,11 +63,7 @@ const operationTypeToLabelMap = { } const operationTypeValidOrThrow = (operationType) => { - if ( - !Object.keys(operationTypeToRedProfileCollectionMethodMap).includes( - operationType - ) - ) { + if (!Object.keys(operationTypeToRedProfileCollectionMethodMap).includes(operationType)) { throw new Error('Invalid operationType given') } return true diff --git a/apps/admin-panel/src/config.js b/apps/admin-panel/src/config.js deleted file mode 100644 index d931a70af..000000000 --- a/apps/admin-panel/src/config.js +++ /dev/null @@ -1,3 +0,0 @@ -export const API_URL = process.env.NX_API_URL - ? process.env.NX_API_URL - : 'http://127.0.0.1:3003/api' diff --git a/apps/admin-panel/src/config.ts b/apps/admin-panel/src/config.ts new file mode 100644 index 000000000..a02bc4249 --- /dev/null +++ b/apps/admin-panel/src/config.ts @@ -0,0 +1 @@ +export const API_URL = process.env.NX_API_URL || 'http://127.0.0.1:3003/api' diff --git a/apps/admin-panel/src/lib/react-admin-loopback/LICENSE b/apps/admin-panel/src/lib/react-admin-loopback/LICENSE deleted file mode 100644 index a00bc98c2..000000000 --- a/apps/admin-panel/src/lib/react-admin-loopback/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2018 Carlos Almeida - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/apps/admin-panel/src/lib/react-admin-loopback/README.md b/apps/admin-panel/src/lib/react-admin-loopback/README.md deleted file mode 100644 index 52caadf43..000000000 --- a/apps/admin-panel/src/lib/react-admin-loopback/README.md +++ /dev/null @@ -1,123 +0,0 @@ -# Loopback Client for react-admin - -For using [Loopback 3](https://loopback.io/) with [react-admin](https://github.com/marmelab/react-admin). - -## Installation - -```bash -npm install --save react-admin-loopback -``` - -## Prerequisite - -- Your loopback server must response `Content-Range` header when querying list. Please use [loopback-content-range](https://github.com/darthwesker/loopback-content-range) on your server end. - -## Usage - -```js -// in src/App.js -import React from 'react' -import { Admin, Resource } from 'react-admin' -import loopbackClient, { authProvider } from 'react-admin-loopback' -import { List, Datagrid, TextField, NumberField } from 'react-admin' - -import { - ShowButton, - EditButton, - Edit, - SimpleForm, - DisabledInput, - TextInput, - NumberInput, -} from 'react-admin' -import { Create } from 'react-admin' -import { Show, SimpleShowLayout } from 'react-admin' - -const BookList = (props) => ( - - - - - - - - -) -export const BookShow = (props) => ( - - - - - - -) -export const BookEdit = (props) => ( - - - - - - - -) -export const BookCreate = (props) => ( - - - - - - -) -const App = () => ( - - - -) - -export default App -``` - -The dataProvider supports include: - -```js -// dataProvider.js -import loopbackProvider from 'react-admin-loopback' - -const dataProvider = loopbackProvider('http://localhost:3000') -export default (type, resource, params) => - new Promise((resolve) => - setTimeout(() => resolve(dataProvider(type, resource, params)), 500) - ) -``` - -```js - -import dataProvider from './dataProvider'; - -... - -dataProvider(GET_LIST, 'books', { - filter: { hide: false }, - sort: { field: 'id', order: 'DESC' }, - pagination: { page: 1, perPage: 0 }, - include: 'users' -}).then(response => { - ... -}); - -... - -``` - -## License - -This library is licensed under the [MIT Licence](LICENSE). diff --git a/apps/admin-panel/src/lib/react-admin-loopback/package.json b/apps/admin-panel/src/lib/react-admin-loopback/package.json deleted file mode 100644 index f782a3a4b..000000000 --- a/apps/admin-panel/src/lib/react-admin-loopback/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "react-admin-loopback", - "version": "1.0.6", - "description": "Packages related to using Loopback with react-admin", - "main": "lib/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "babel ./src -d lib --ignore '*.spec.js'" - }, - "files": [ - "*.md", - "lib", - "src" - ], - "devDependencies": { - "babel-cli": "^6.23.0", - "babel-core": "^6.23.1", - "babel-plugin-transform-object-rest-spread": "^6.23.0", - "babel-preset-env": "^1.7.0", - "babel-preset-es2015": "^6.24.1", - "babel-preset-stage-0": "^6.24.1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/darthwesker/react-admin-loopback.git" - }, - "keywords": [ - "react", - "react-admin", - "loopback", - "rest-client" - ], - "author": "Carlos Almeida", - "license": "MIT", - "bugs": { - "url": "https://github.com/darthwesker/react-admin-loopback/issues" - }, - "homepage": "https://github.com/darthwesker/react-admin-loopback#readme" -} diff --git a/apps/admin-panel/src/lib/react-admin-loopback/src/authProvider.js b/apps/admin-panel/src/lib/react-admin-loopback/src/authProvider.ts similarity index 73% rename from apps/admin-panel/src/lib/react-admin-loopback/src/authProvider.js rename to apps/admin-panel/src/lib/react-admin-loopback/src/authProvider.ts index abcd178c2..0b41d5904 100644 --- a/apps/admin-panel/src/lib/react-admin-loopback/src/authProvider.js +++ b/apps/admin-panel/src/lib/react-admin-loopback/src/authProvider.ts @@ -3,11 +3,11 @@ import storage from './storage' /* eslint-disable prefer-promise-reject-errors */ -export const authProvider = (loginApiUrl, noAccessPage = '/login') => { +export const authProvider = (loginApiUrl: RequestInfo, noAccessPage: string = '/login') => { return (type, params) => { - if (params && !params.email && params.username) { + if (params?.username && !params.email) { params.email = params.username - delete params.username + delete params.username //TODO avoid delete } if (type === AUTH_LOGIN) { const request = new Request(loginApiUrl, { @@ -17,9 +17,8 @@ export const authProvider = (loginApiUrl, noAccessPage = '/login') => { }) return fetch(request) .then((response) => { - if (response.status < 200 || response.status >= 300) { + if (response.status < 200 || response.status >= 300) throw new Error(response.statusText) - } return response.json() }) .then(({ ttl, ...data }) => { @@ -40,12 +39,9 @@ export const authProvider = (loginApiUrl, noAccessPage = '/login') => { } if (type === AUTH_CHECK) { const token = storage.load('lbtoken') - if (token && token.id) { - return Promise.resolve() - } else { - storage.remove('lbtoken') - return Promise.reject({ redirectTo: noAccessPage }) - } + if (token?.id) return Promise.resolve() + storage.remove('lbtoken') + return Promise.reject({ redirectTo: noAccessPage }) } return Promise.reject('Unknown method') } diff --git a/apps/admin-panel/src/lib/react-admin-loopback/src/fetch.js b/apps/admin-panel/src/lib/react-admin-loopback/src/fetch.ts similarity index 69% rename from apps/admin-panel/src/lib/react-admin-loopback/src/fetch.js rename to apps/admin-panel/src/lib/react-admin-loopback/src/fetch.ts index dd03b51ce..8d85942b3 100644 --- a/apps/admin-panel/src/lib/react-admin-loopback/src/fetch.js +++ b/apps/admin-panel/src/lib/react-admin-loopback/src/fetch.ts @@ -1,16 +1,25 @@ import { HttpError } from 'react-admin' import storage from './storage' -const fetchJson = async (url, options = {}) => { +interface FetchOptions { + user?: { + authenticated: boolean, + token: string, + } + headers?: Headers + body?: BodyInit +} + +const fetchJson = async (url: RequestInfo, options: FetchOptions = {}) => { const requestHeaders = options.headers || new Headers({ Accept: 'application/json' }) if ( !requestHeaders.has('Content-Type') && - !(options && options.body && options.body instanceof FormData) + !(options?.body && options.body instanceof FormData) ) { requestHeaders.set('Content-Type', 'application/json') } - if (options.user && options.user.authenticated && options.user.token) { + if (options.user?.authenticated && options.user.token) { requestHeaders.set('Authorization', options.user.token) } const response = await fetch(url, { ...options, headers: requestHeaders }) @@ -26,6 +35,7 @@ const fetchJson = async (url, options = {}) => { const headers = o.headers const body = o.body let json + try { json = JSON.parse(body) } catch (e) { @@ -34,21 +44,21 @@ const fetchJson = async (url, options = {}) => { if (status < 200 || status >= 300) { return Promise.reject( new HttpError( - (json && json.error && json.error.message) || statusText, + json?.error?.message || statusText, status, json ) ) } return Promise.resolve({ - status: status, - headers: headers, - body: body, - json: json, + status, + headers, + body, + json, }) } -export default (url, options = {}) => { +export default function (url: RequestInfo, options: FetchOptions = {}) { options.user = { authenticated: true, token: storage.load('lbtoken').id, diff --git a/apps/admin-panel/src/lib/react-admin-loopback/src/index.js b/apps/admin-panel/src/lib/react-admin-loopback/src/index.js deleted file mode 100644 index 5c6edd57d..000000000 --- a/apps/admin-panel/src/lib/react-admin-loopback/src/index.js +++ /dev/null @@ -1,179 +0,0 @@ -import { stringify } from 'query-string' -import fetchJson from './fetch' - -import { - GET_LIST, - GET_ONE, - GET_MANY, - GET_MANY_REFERENCE, - CREATE, - UPDATE, - UPDATE_MANY, - DELETE, - DELETE_MANY, -} from 'react-admin' - -export * from './authProvider' -export { default as storage } from './storage' - -export default (apiUrl, httpClient = fetchJson) => { - /** - * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' - * @param {String} resource Name of the resource to fetch, e.g. 'posts' - * @param {Object} params The data request params, depending on the type - * @returns {Object} { url, options } The HTTP request parameters - */ - const convertDataRequestToHTTP = (type, resource, params) => { - let url = '' - const options = {} - const specialParams = ['pagination', 'sort', 'filter'] - switch (type) { - case GET_LIST: { - console.log(params) - const { page, perPage } = params.pagination - const { field, order } = params.sort - const query = {} - query.where = { ...params.filter } - if (field) query.order = [field + ' ' + order] - if (perPage >= 0) query.limit = perPage - if (perPage > 0 && page >= 0) query.skip = (page - 1) * perPage - - Object.keys(params).forEach((key) => { - if (!specialParams.includes(key) && params[key] !== undefined) { - query[key] = params[key] - } - }) - url = `${apiUrl}/${resource}?${stringify({ - filter: JSON.stringify(query), - })}` - break - } - case GET_ONE: - url = `${apiUrl}/${resource}/${params.id}` - break - case GET_MANY: { - const listId = params.ids.map((id) => { - return { id } - }) - - let query = '' - if (listId.length > 0) { - const filter = { - where: { or: listId }, - } - query = `?${stringify({ filter: JSON.stringify(filter) })}` - } - url = `${apiUrl}/${resource}${query}` - break - } - case GET_MANY_REFERENCE: { - const { page, perPage } = params.pagination - const { field, order } = params.sort - const query = {} - query.where = { ...params.filter } - query.where[params.target] = params.id - if (field) query.order = [field + ' ' + order] - if (perPage >= 0) query.limit = perPage - if (perPage > 0 && page >= 0) query.skip = (page - 1) * perPage - - Object.keys(params).forEach((key) => { - if (!specialParams.includes(key) && params[key] !== undefined) { - query[key] = params[key] - } - }) - - url = `${apiUrl}/${resource}?${stringify({ - filter: JSON.stringify(query), - })}` - break - } - case UPDATE: - url = `${apiUrl}/${resource}/${params.id}` - options.method = 'PATCH' - options.body = JSON.stringify(params.data) - break - case CREATE: - url = `${apiUrl}/${resource}` - options.method = 'POST' - options.body = JSON.stringify(params.data) - break - case DELETE: - url = `${apiUrl}/${resource}/${params.id}` - options.method = 'DELETE' - break - default: - throw new Error(`Unsupported fetch action type ${type}`) - } - return { url, options } - } - - /** - * @param {Object} response HTTP response from fetch() - * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' - * @param {String} resource Name of the resource to fetch, e.g. 'posts' - * @param {Object} params The data request params, depending on the type - * @returns {Object} Data response - */ - const convertHTTPResponse = (response, type, resource, params) => { - const { headers, json } = response - switch (type) { - case GET_LIST: - case GET_MANY_REFERENCE: - if (!headers.has('content-range')) { - throw new Error( - 'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?' - ) - } - return { - data: json, - total: parseInt(headers.get('content-range').split('/').pop(), 10), - } - case CREATE: - return { data: { ...params.data, id: json.id } } - case DELETE: - return { data: { ...json, id: params.id } } - default: - return { data: json } - } - } - - /** - * @param {string} type Request type, e.g GET_LIST - * @param {string} resource Resource name, e.g. "posts" - * @param {Object} payload Request parameters. Depends on the request type - * @returns {Promise} the Promise for a data response - */ - return (type, resource, params) => { - // simple-rest doesn't handle filters on UPDATE route, so we fallback to calling UPDATE n times instead - if (type === UPDATE_MANY) { - return Promise.all( - params.ids.map((id) => - httpClient(`${apiUrl}/${resource}/${id}`, { - method: 'PUT', - body: JSON.stringify(params.data), - }) - ) - ).then((responses) => ({ - data: responses.map((response) => response.json), - })) - } - // simple-rest doesn't handle filters on DELETE route, so we fallback to calling DELETE n times instead - if (type === DELETE_MANY) { - return Promise.all( - params.ids.map((id) => - httpClient(`${apiUrl}/${resource}/${id}`, { - method: 'DELETE', - }) - ) - ).then((responses) => ({ - data: responses.map((response) => response.json), - })) - } - - const { url, options } = convertDataRequestToHTTP(type, resource, params) - - return httpClient(url, options).then((response) => - convertHTTPResponse(response, type, resource, params) - ) - } -} diff --git a/apps/admin-panel/src/lib/react-admin-loopback/src/index.ts b/apps/admin-panel/src/lib/react-admin-loopback/src/index.ts new file mode 100644 index 000000000..838f737d8 --- /dev/null +++ b/apps/admin-panel/src/lib/react-admin-loopback/src/index.ts @@ -0,0 +1,193 @@ +import { stringify } from 'query-string'; +import fetchJson from './fetch'; + +import { + GET_LIST, + GET_ONE, + GET_MANY, + GET_MANY_REFERENCE, + CREATE, + UPDATE, + UPDATE_MANY, + DELETE, + DELETE_MANY, +} from 'react-admin'; +import { objectKeys } from '@talent-connect/typescript-utilities'; + +export * from './authProvider'; +export { default as storage } from './storage'; + +interface RequestParams { + pagination: Record<'page' | 'perPage', number>; + sort: { + field: unknown; + order: unknown; + } + ids: string[]; + target: string; + id: string; + data: unknown; + filter: unknown; +} + +export default function (apiUrl: string, httpClient = fetchJson) { + /** + * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' + * @param {String} resource Name of the resource to fetch, e.g. 'posts' + * @param {Object} params The data request params, depending on the type + * @returns {Object} { url, options } The HTTP request parameters + */ + const convertDataRequestToHTTP = (type: string, resource: string, params: RequestParams) => { + let url = ''; + const options = {}; + const specialParams = ['pagination', 'sort', 'filter']; + switch (type) { + case GET_LIST: { + console.log(params); + const { page, perPage } = params.pagination; + const { field, order } = params.sort; + const query = {}; + query.where = { ...params.filter }; + if (field) query.order = [field + ' ' + order]; + if (perPage >= 0) query.limit = perPage; + if (perPage > 0 && page >= 0) query.skip = (page - 1) * perPage; + + Object.keys(params).forEach((key) => { + if (!specialParams.includes(key) && !!params[key]) { + query[key] = params[key]; + } + }); + url = `${apiUrl}/${resource}?${stringify({ filter: JSON.stringify(query) })}`; + break; + } + case GET_ONE: + url = `${apiUrl}/${resource}/${params.id}`; + break; + case GET_MANY: { + const listId = params.ids.map((id) => id); + + let query = ''; + if (listId.length) { + const filter = { + where: { or: listId }, + }; + query = `?${stringify({ filter: JSON.stringify(filter) })}`; + } + url = `${apiUrl}/${resource}${query}`; + break; + } + case GET_MANY_REFERENCE: { + const { page, perPage } = params.pagination; + const { field, order } = params.sort; + const query = {}; + query.where = { ...params.filter }; + query.where[params.target] = params.id; + if (field) query.order = [field + ' ' + order]; + if (perPage >= 0) query.limit = perPage; + if (perPage > 0 && page >= 0) query.skip = (page - 1) * perPage; + + objectKeys(params).forEach((key) => { + if (!specialParams.includes(key) && !!params[key]) + query[key] = params[key]; + }); + + url = `${apiUrl}/${resource}?${stringify({ + filter: JSON.stringify(query), + })}`; + break; + } + case UPDATE: + url = `${apiUrl}/${resource}/${params.id}`; + options.method = 'PATCH'; + options.body = JSON.stringify(params.data); + break; + case CREATE: + url = `${apiUrl}/${resource}`; + options.method = 'POST'; + options.body = JSON.stringify(params.data); + break; + case DELETE: + url = `${apiUrl}/${resource}/${params.id}`; + options.method = 'DELETE'; + break; + default: + throw new Error(`Unsupported fetch action type ${type}`); + } + return { url, options }; + }; + + /** + * @param {Object} response HTTP response from fetch() + * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' + * @param {String} resource Name of the resource to fetch, e.g. 'posts' + * @param {Object} params The data request params, depending on the type + * @returns {Object} Data response + */ + const convertHTTPResponse = (response, type: string, resource: string, params) => { + const { headers, json } = response; + switch (type) { + case GET_LIST: + case GET_MANY_REFERENCE: + if (!headers.has('content-range')) + throw new Error( + `The Content-Range header is missing in the HTTP Response. + The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. + If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?`); + + return { + data: json, + total: parseInt(headers.get('content-range').split('/').pop(), 10), + }; + case CREATE: + return { data: { ...params.data, id: json.id } }; + case DELETE: + return { data: { ...json, id: params.id } }; + default: + return { data: json }; + } + }; + + /** + * @returns {Promise} the Promise for a data response + */ + return async function ( + /** Request type, e.g GET_LIST */ + type: string, + /** Resource name, e.g. "posts" */ + resource: string, + /** Request parameters. Depends on the request type */ + params: { ids: string[]; data: object; } + ): Promise<{ data: unknown[]; }> { // TODO: type + // simple-rest doesn't handle filters on UPDATE route, so we fallback to calling UPDATE n times instead + if (type === UPDATE_MANY) { + return Promise.all( + params.ids.map((id) => + httpClient(`${apiUrl}/${resource}/${id}`, { + method: 'PUT', + body: JSON.stringify(params.data), + }) + ) + ).then((responses) => ({ + data: responses.map(({ json }) => json), + })); + } + // simple-rest doesn't handle filters on DELETE route, so we fallback to calling DELETE n times instead + if (type === DELETE_MANY) { + return Promise.all( + params.ids.map((id) => + httpClient(`${apiUrl}/${resource}/${id}`, { + method: 'DELETE', + }) + ) + ).then((responses) => ({ + data: responses.map(({ json }) => json), + })); + } + + const { url, options } = convertDataRequestToHTTP(type, resource, params); + + return httpClient(url, options).then((response) => + convertHTTPResponse(response, type, resource, params) + ); + }; +}; diff --git a/apps/admin-panel/src/lib/react-admin-loopback/src/storage.js b/apps/admin-panel/src/lib/react-admin-loopback/src/storage.js deleted file mode 100644 index 0164e825f..000000000 --- a/apps/admin-panel/src/lib/react-admin-loopback/src/storage.js +++ /dev/null @@ -1,51 +0,0 @@ -export default { - save: function (key, value, expirationSec) { - if (typeof Storage === 'undefined') { - return false - } - var expirationMS = expirationSec * 1000 - var record = { - value: value, - timestamp: new Date().getTime() + expirationMS, - } - localStorage.setItem(key, JSON.stringify(record)) - - return value - }, - load: function (key) { - if (typeof Storage === 'undefined') { - return false - } - try { - var record = JSON.parse(localStorage.getItem(key)) - if (!record) { - return false - } - return new Date().getTime() < record.timestamp && record.value - } catch (e) { - return false - } - }, - remove: function (key) { - if (typeof Storage === 'undefined') { - return false - } - localStorage.removeItem(key) - }, - update: function (key, value) { - if (typeof Storage === 'undefined') { - return false - } - try { - var record = JSON.parse(localStorage.getItem(key)) - if (!record) { - return false - } - var updatedRecord = { value: value, timestamp: record.timestamp } - localStorage.setItem(key, JSON.stringify(updatedRecord)) - return updatedRecord - } catch (e) { - return false - } - }, -} diff --git a/apps/admin-panel/src/lib/react-admin-loopback/src/storage.ts b/apps/admin-panel/src/lib/react-admin-loopback/src/storage.ts new file mode 100644 index 000000000..00162ee28 --- /dev/null +++ b/apps/admin-panel/src/lib/react-admin-loopback/src/storage.ts @@ -0,0 +1,43 @@ +export default { + /** */ + save: function (key: string, value, expirationSec: number) { + if (!Storage) return false + var expirationMS = expirationSec * 1000 + var record = { + value, + timestamp: new Date().getTime() + expirationMS, + } + localStorage.setItem(key, JSON.stringify(record)) + + return value + }, + /** */ + load: function (key: string) { + if (!Storage) return false + try { + var record = JSON.parse(localStorage.getItem(key)) + if (!record) return false + return record.value && new Date().getTime() < record.timestamp + } catch (e) { + return false + } + }, + /** */ + remove: function (key: string) { + if (!Storage) return false + localStorage.removeItem(key) + }, + /** */ + update: function (key: string, value) { + if (!Storage) return false + try { + var record = JSON.parse(localStorage.getItem(key)) + if (!record) return false + const updatedRecord = { value, timestamp: record.timestamp } + localStorage.setItem(key, JSON.stringify(updatedRecord)) + return updatedRecord + } catch (e) { + return false + } + }, +} diff --git a/apps/admin-panel/src/main.tsx b/apps/admin-panel/src/main.tsx index ca79c86cf..8fd36a1a8 100644 --- a/apps/admin-panel/src/main.tsx +++ b/apps/admin-panel/src/main.tsx @@ -1,4 +1,3 @@ -import React from 'react' import ReactDOM from 'react-dom' import App from './App' import { API_URL } from './config' diff --git a/apps/admin-panel/src/utils/csvExport.js b/apps/admin-panel/src/utils/csvExport.ts similarity index 100% rename from apps/admin-panel/src/utils/csvExport.js rename to apps/admin-panel/src/utils/csvExport.ts diff --git a/apps/api/common/models/red-match.js b/apps/api/common/models/red-match.js index b6a4e6a69..cfe3d1ac3 100644 --- a/apps/api/common/models/red-match.js +++ b/apps/api/common/models/red-match.js @@ -1,11 +1,11 @@ -'use strict' +'use strict'; -const Rx = require('rxjs') -const { switchMap } = require('rxjs/operators') +const Rx = require('rxjs'); +const { switchMap } = require('rxjs/operators'); -const { DateTime } = require('luxon') +const { DateTime } = require('luxon'); -const app = require('../../server/server') +const app = require('../../server/server'); const { sendMentorshipRequestReceivedEmail, sendMentorshipDeclinedEmail, @@ -13,7 +13,7 @@ const { sendMentorshipCompletionEmailToMentor, sendMentorshipCompletionEmailToMentee, sendNotificationToMentorThatPendingApplicationExpiredSinceOtherMentorAccepted, -} = require('../../lib/email/email') +} = require('../../lib/email/email'); module.exports = function (RedMatch) { /** @@ -22,16 +22,16 @@ module.exports = function (RedMatch) { * @param {Function(Error, object)} callback */ - RedMatch.observe('before save', async function updateTimestamp(ctx, next) { - if (process.env.NODE_ENV === 'seeding') return next() + RedMatch.observe('before save', async function updateTimestamp (ctx, next) { + if (process.env.NODE_ENV === 'seeding') return next(); if (ctx.instance) { if (ctx.isNewInstance) { - ctx.instance.createdAt = new Date() - ctx.instance.hasMenteeDismissedMentorshipApplicationAcceptedNotification = false + ctx.instance.createdAt = new Date(); + ctx.instance.hasMenteeDismissedMentorshipApplicationAcceptedNotification = false; } - ctx.instance.updatedAt = new Date() + ctx.instance.updatedAt = new Date(); } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); } // if current user is admin, don't run any of the logic below @@ -40,7 +40,7 @@ module.exports = function (RedMatch) { ctx.options.currentUser && ctx.options.currentUser.email === 'cloud-accounts@redi-school.org' ) { - return next() + return next(); } // Note: tricky handling of the .rediLocation property coming up. This fix was done on 17 May 2020. @@ -54,27 +54,27 @@ module.exports = function (RedMatch) { // If so, we allow it to stay the way it is. This does open the door for users specifying this by themselves, // but given the usage of ReDI Connect, this is an "acceptable risk". However, whenever Loopback as the backend // is replaced, make sure to TODO: replace it. - const RedProfile = app.models.RedProfile + const RedProfile = app.models.RedProfile; if (ctx.instance) { - if (ctx.isNewInstance) ctx.instance.createdAt = new Date() - ctx.instance.updatedAt = new Date() + if (ctx.isNewInstance) ctx.instance.createdAt = new Date(); + ctx.instance.updatedAt = new Date(); if (!ctx.instance.rediLocation) { - const mentee = await RedProfile.findById(ctx.instance.menteeId) - const menteeRediLocation = mentee.toJSON().rediLocation - ctx.instance.rediLocation = menteeRediLocation + const mentee = await RedProfile.findById(ctx.instance.menteeId); + const menteeRediLocation = mentee.toJSON().rediLocation; + ctx.instance.rediLocation = menteeRediLocation; } } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); if (!ctx.data.rediLocation) { - const mentee = await RedProfile.findById(ctx.data.menteeId) - const menteeRediLocation = mentee.toJSON().rediLocation - ctx.data.rediLocation = menteeRediLocation + const mentee = await RedProfile.findById(ctx.data.menteeId); + const menteeRediLocation = mentee.toJSON().rediLocation; + ctx.data.rediLocation = menteeRediLocation; } } - next() - }) + next(); + }); // IMPORTANT ACL-related method: this combines with the ACL $authenticated-can-execute-find, // to allow $mentor and $mentee to look up RedMatch. To make sure a mentee user can only @@ -82,16 +82,16 @@ module.exports = function (RedMatch) { // is used. RedMatch.observe( 'access', - function onlyMatchesRelatedToCurrentUser(ctx, next) { - if (!ctx.options.currentUser) return next() + function onlyMatchesRelatedToCurrentUser (ctx, next) { + if (!ctx.options.currentUser) return next(); - const currentUserProfileId = ctx.options.currentUser.redProfile.id + const currentUserProfileId = ctx.options.currentUser.redProfile.id; // TODO: this one is included (as of writing) only for convenience in admin panel. // Considering putting in a if(adminUser) block - ctx.query.include = ['mentor', 'mentee'] + ctx.query.include = ['mentor', 'mentee']; - if (!ctx.query.where) ctx.query.where = {} + if (!ctx.query.where) ctx.query.where = {}; // TODO: Replace this with role-based 'admin' role check if (ctx.options.currentUser.email !== 'cloud-accounts@redi-school.org') { @@ -100,45 +100,44 @@ module.exports = function (RedMatch) { { mentorId: currentUserProfileId }, { menteeId: currentUserProfileId }, ], - } - const existingWhere = ctx.query.where - if (Object.values(existingWhere).length > 0) { - ctx.query.where = { and: [currentUserMenteeOrMentor, existingWhere] } - } else { - ctx.query.where = currentUserMenteeOrMentor - } + }; + const existingWhere = ctx.query.where; + ctx.query.where = Object.values(existingWhere).length + ? { and: [currentUserMenteeOrMentor, existingWhere] } + : currentUserMenteeOrMentor; } - next() + next(); } - ) + ); RedMatch.acceptMentorship = async (data, options, callback) => { - const { redMatchId, mentorReplyMessageOnAccept } = data + const { redMatchId, mentorReplyMessageOnAccept } = data; - const RedProfile = app.models.RedProfile + const RedProfile = app.models.RedProfile; + const rediLocation = options.currentUser.redProfile.rediLocation; - let redMatch = await RedMatch.findById(redMatchId) - const redMatchData = redMatch.toJSON() + let redMatch = await RedMatch.findById(redMatchId); + const redMatchData = redMatch.toJSON(); const [mentor, mentee] = await Promise.all([ RedProfile.findById(redMatchData.mentorId), RedProfile.findById(redMatchData.menteeId), - ]) + ]); redMatch = await redMatch.updateAttributes({ status: 'accepted', matchMadeActiveOn: DateTime.utc().toString(), mentorReplyMessageOnAccept, - rediLocation: options.currentUser.redProfile.rediLocation, - }) + rediLocation + }); await sendMentorshipAcceptedEmail({ recipient: [mentee.contactEmail, mentor.contactEmail], mentorName: mentor.firstName, menteeName: mentee.firstName, mentorReplyMessageOnAccept: mentorReplyMessageOnAccept, - rediLocation: options.currentUser.redProfile.rediLocation, - }).toPromise() + rediLocation + }).toPromise(); const menteePendingMatches = await RedMatch.find({ where: { @@ -146,36 +145,33 @@ module.exports = function (RedMatch) { status: 'applied', }, include: ['mentee', 'mentor'], - }) + }); await Promise.all( menteePendingMatches.map((pendingMatch) => { return pendingMatch.updateAttributes({ status: 'invalidated-as-other-mentor-accepted', - rediLocation: options.currentUser.redProfile.rediLocation, - }) + rediLocation + }); }) - ) + ); await Promise.all( menteePendingMatches.map((pendingMatch) => { - const pendingMatchData = pendingMatch.toJSON() + const { mentor, mentee } = pendingMatch.toJSON(); return sendNotificationToMentorThatPendingApplicationExpiredSinceOtherMentorAccepted( { - recipient: pendingMatchData.mentor.contactEmail, - mentorName: pendingMatchData.mentor.firstName, - menteeName: - pendingMatchData.mentee.firstName + - ' ' + - pendingMatchData.mentee.lastName, - rediLocation: options.currentUser.redProfile.rediLocation, + recipient: mentor.contactEmail, + mentorName: mentor.firstName, + menteeName: `${mentee.firstName} ${mentee.lastName}`, + rediLocation } - ).toPromise() + ).toPromise(); }) - ) + ); - return redMatch.toJSON() - } + return redMatch.toJSON(); + }; RedMatch.declineMentorship = async (data, options, callback) => { const { @@ -183,16 +179,16 @@ module.exports = function (RedMatch) { ifDeclinedByMentor_chosenReasonForDecline, ifDeclinedByMentor_ifReasonIsOther_freeText, ifDeclinedByMentor_optionalMessageToMentee, - } = data + } = data; - const RedProfile = app.models.RedProfile + const RedProfile = app.models.RedProfile; - let redMatch = await RedMatch.findById(redMatchId) - const redMatchData = redMatch.toJSON() + let redMatch = await RedMatch.findById(redMatchId); + const redMatchData = redMatch.toJSON(); const [mentor, mentee] = await Promise.all([ RedProfile.findById(redMatchData.mentorId), RedProfile.findById(redMatchData.menteeId), - ]) + ]); redMatch = await redMatch.updateAttributes({ status: 'declined-by-mentor', @@ -201,7 +197,7 @@ module.exports = function (RedMatch) { ifDeclinedByMentor_optionalMessageToMentee, ifDeclinedByMentor_dateTime: DateTime.utc().toString(), rediLocation: options.currentUser.redProfile.rediLocation, - }) + }); await sendMentorshipDeclinedEmail({ recipient: mentee.contactEmail, @@ -210,51 +206,51 @@ module.exports = function (RedMatch) { ifDeclinedByMentor_chosenReasonForDecline, ifDeclinedByMentor_ifReasonIsOther_freeText, ifDeclinedByMentor_optionalMessageToMentee, - }).toPromise() + }).toPromise(); - return redMatch.toJSON() - } + return redMatch.toJSON(); + }; RedMatch.markAsCompleted = async (data, options, callback) => { - const { redMatchId, mentorMessageOnComplete } = data + const { redMatchId, mentorMessageOnComplete } = data; - const RedProfile = app.models.RedProfile + const RedProfile = app.models.RedProfile; - let redMatch = await RedMatch.findById(redMatchId) - const redMatchData = redMatch.toJSON() + let redMatch = await RedMatch.findById(redMatchId); + const redMatchData = redMatch.toJSON(); const [mentor, mentee] = await Promise.all([ RedProfile.findById(redMatchData.mentorId), RedProfile.findById(redMatchData.menteeId), - ]) + ]); redMatch = await redMatch.updateAttributes({ status: 'completed', matchCompletedOn: DateTime.utc().toString(), mentorMessageOnComplete, rediLocation: options.currentUser.redProfile.rediLocation, - }) + }); await sendMentorshipCompletionEmailToMentor({ recipient: mentor.contactEmail, mentorFirstName: mentor.firstName, menteeFirstName: mentee.firstName, - }).toPromise() + }).toPromise(); await sendMentorshipCompletionEmailToMentee({ recipient: mentee.contactEmail, mentorFirstName: mentor.firstName, menteeFirstName: mentee.firstName, - }).toPromise() + }).toPromise(); - return redMatch.toJSON() - } + return redMatch.toJSON(); + }; RedMatch.requestMentorship = function (data, options, callback) { - const { applicationText, expectationText, mentorId } = data - const redMatchCreate = Rx.bindNodeCallback(RedMatch.create.bind(RedMatch)) + const { applicationText, expectationText, mentorId } = data; + const redMatchCreate = Rx.bindNodeCallback(RedMatch.create.bind(RedMatch)); const redProfileFind = Rx.bindNodeCallback( app.models.RedProfile.findOne.bind(app.models.RedProfile) - ) + ); // TODO: enforce following rules // 1. Requesting user must be a mentee // 2. Requested mentor must be an actual mentor @@ -267,7 +263,7 @@ module.exports = function (RedMatch) { mentorId, menteeId: options.currentUser.redProfile.id, rediLocation: options.currentUser.redProfile.rediLocation, - } + }; redProfileFind({ where: { id: mentorId } }) .pipe( switchMap((mentorProfile) => @@ -280,27 +276,27 @@ module.exports = function (RedMatch) { }) ) ) - .subscribe() + .subscribe(); redMatchCreate(redMatch).subscribe( (inst) => callback(null, inst), (err) => callback(err) - ) - } + ); + }; RedMatch.markAsDismissed = async function (data, options, callback) { - const { redMatchId } = data + const { redMatchId } = data; try { - let redMatch = await RedMatch.findById(redMatchId) + let redMatch = await RedMatch.findById(redMatchId); redMatch = await redMatch.updateAttributes({ hasMenteeDismissedMentorshipApplicationAcceptedNotification: true, rediLocation: options.currentUser.redProfile.rediLocation, - }) + }); - return redMatch.toJSON() + return redMatch.toJSON(); } catch (err) { - throw err + throw err; } - } -} + }; +}; diff --git a/apps/api/common/models/red-mentoring-session.js b/apps/api/common/models/red-mentoring-session.js index 117ede5f8..3bdf9bf80 100644 --- a/apps/api/common/models/red-mentoring-session.js +++ b/apps/api/common/models/red-mentoring-session.js @@ -1,10 +1,10 @@ -'use strict' +'use strict'; -const Rx = require('rxjs') -const { switchMap } = require('rxjs/operators') +const Rx = require('rxjs'); +const { switchMap } = require('rxjs/operators'); -const app = require('../../server/server') -const { sendMentoringSessionLoggedEmail } = require('../../lib/email/email') +const app = require('../../server/server'); +const { sendMentoringSessionLoggedEmail } = require('../../lib/email/email'); module.exports = function (RedMentoringSession) { // IMPORTANT ACL-related method: this combines with the ACL $authenticated-can-execute-find, @@ -13,16 +13,16 @@ module.exports = function (RedMentoringSession) { // is used. RedMentoringSession.observe( 'access', - function onlyMatchesRelatedToCurrentUser(ctx, next) { - if (!ctx.options.currentUser) return next() + function onlyMatchesRelatedToCurrentUser (ctx, next) { + if (!ctx.options.currentUser) return next(); - const currentUserProfileId = ctx.options.currentUser.redProfile.id + const currentUserProfileId = ctx.options.currentUser.redProfile.id; // TODO: this one is included (as of writing) only for convenience in admin panel. // Considering putting in a if(adminUser) block - ctx.query.include = ['mentor', 'mentee'] + ctx.query.include = ['mentor', 'mentee']; - if (!ctx.query.where) ctx.query.where = {} + if (!ctx.query.where) ctx.query.where = {}; // TODO: Replace this with role-based 'admin' role check if (ctx.options.currentUser.email !== 'cloud-accounts@redi-school.org') { @@ -31,29 +31,27 @@ module.exports = function (RedMentoringSession) { { mentorId: currentUserProfileId }, { menteeId: currentUserProfileId }, ], - } - const existingWhere = ctx.query.where - if (Object.values(existingWhere).length > 0) { - ctx.query.where = { and: [currentUserMenteeOrMentor, existingWhere] } - } else { - ctx.query.where = currentUserMenteeOrMentor - } + }; + const existingWhere = ctx.query.where; + ctx.query.where = Object.values(existingWhere).length + ? { and: [currentUserMenteeOrMentor, existingWhere] } + : currentUserMenteeOrMentor; } - next() + next(); } - ) + ); RedMentoringSession.observe( 'before save', - async function updateTimestamp(ctx, next) { + async function updateTimestamp (ctx, next) { if (ctx.options.currentUser.email !== 'cloud-accounts@redi-school.org') { if (ctx.instance) { if (ctx.isNewInstance) { - ctx.instance.mentorId = ctx.options.currentUser.redProfile.id + ctx.instance.mentorId = ctx.options.currentUser.redProfile.id; const findOneRedProfile = Rx.bindNodeCallback( app.models.RedProfile.findOne.bind(app.models.RedProfile) - ) + ); Rx.zip( findOneRedProfile({ where: { id: ctx.instance.mentorId } }), findOneRedProfile({ where: { id: ctx.instance.menteeId } }) @@ -69,35 +67,35 @@ module.exports = function (RedMentoringSession) { }) ) ) - .subscribe() + .subscribe(); } } } if (ctx.instance) { if (ctx.isNewInstance) { - ctx.instance.createdAt = new Date() + ctx.instance.createdAt = new Date(); } - ctx.instance.updatedAt = new Date() + ctx.instance.updatedAt = new Date(); } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); } - const RedProfile = app.models.RedProfile + const RedProfile = app.models.RedProfile; if (process.env.NODE_ENV !== 'seeding') { if (ctx.instance) { - const mentee = await RedProfile.findById(ctx.instance.menteeId) - const menteeRediLocation = mentee.toJSON().rediLocation - ctx.instance.rediLocation = menteeRediLocation + const mentee = await RedProfile.findById(ctx.instance.menteeId); + const menteeRediLocation = mentee.toJSON().rediLocation; + ctx.instance.rediLocation = menteeRediLocation; } else { - const mentee = await RedProfile.findById(ctx.data.menteeId) - const menteeRediLocation = mentee.toJSON().rediLocation - ctx.data.rediLocation = menteeRediLocation + const mentee = await RedProfile.findById(ctx.data.menteeId); + const menteeRediLocation = mentee.toJSON().rediLocation; + ctx.data.rediLocation = menteeRediLocation; } } - next() + next(); } - ) -} + ); +}; diff --git a/apps/api/common/models/red-profile.js b/apps/api/common/models/red-profile.js index 87e376d23..7efaba1ca 100644 --- a/apps/api/common/models/red-profile.js +++ b/apps/api/common/models/red-profile.js @@ -1,176 +1,164 @@ -'use strict' -const _ = require('lodash') +'use strict'; +const _ = require('lodash'); -const Rx = require('rxjs') -const { of } = Rx -const { switchMap, map } = require('rxjs/operators') -const { DateTime } = require('luxon') +const Rx = require('rxjs'); +const { of } = Rx; +const { switchMap, map } = require('rxjs/operators'); +const { DateTime } = require('luxon'); -const app = require('../../server/server') +const app = require('../../server/server'); const { sendMentorPendingReviewAcceptedEmail, sendMenteePendingReviewAcceptedEmail, sendPendingReviewDeclinedEmail, sendVerificationEmail, - sendEmailToUserWithTpJobseekerProfileSigningUpToCon, -} = require('../../lib/email/email') + sendEmailToUserWithTpJobSeekerProfileSigningUpToCon, +} = require('../../lib/email/email'); const addFullNamePropertyForAdminSearch = (ctx) => { - let thingToUpdate - if (ctx.instance) thingToUpdate = ctx.instance - else if (ctx.data) thingToUpdate = ctx.data - else return + let thingToUpdate; + if (ctx.instance) thingToUpdate = ctx.instance; + else if (ctx.data) thingToUpdate = ctx.data; + else return; - const firstName = thingToUpdate.firstName - const lastName = thingToUpdate.lastName + const firstName = thingToUpdate.firstName; + const lastName = thingToUpdate.lastName; if (firstName || lastName) { - const merged = `${firstName ? firstName + ' ' : ''}${lastName || ''}` + const merged = `${firstName ? firstName + ' ' : ''}${lastName || ''}`; thingToUpdate.loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName = - merged + merged; } -} +}; module.exports = function (RedProfile) { - RedProfile.observe('before save', function updateTimestamp(ctx, next) { - addFullNamePropertyForAdminSearch(ctx) - const currentDate = new Date() + RedProfile.observe('before save', function updateTimestamp (ctx, next) { + addFullNamePropertyForAdminSearch(ctx); + const currentDate = new Date(); if (ctx.instance) { if (ctx.isNewInstance) { - ctx.instance.createdAt = currentDate - ctx.instance.gaveGdprConsentAt = currentDate + ctx.instance.createdAt = currentDate; + ctx.instance.gaveGdprConsentAt = currentDate; } - ctx.instance.updatedAt = new Date() + ctx.instance.updatedAt = new Date(); } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); } - next() - }) + next(); + }); - RedProfile.observe('loaded', function loadRedMatchCount(ctx, next) { + RedProfile.observe('loaded', function loadRedMatchCount (ctx, next) { if (ctx.isNewInstance) { - return next() + return next(); // TODO: the next two else-if blocks can definitely be DRY-ed. Merge them. } - if ( - ctx.options && - ctx.options.currentUser && - ctx.options.currentUser.email === 'cloud-accounts@redi-school.org' - ) { - } else { - return next() - } + if (ctx.options?.currentUser?.email !== 'cloud-accounts@redi-school.org') + return next(); - const RedMatch = app.models.RedMatch - const AccessToken = app.models.AccessToken + const RedMatch = app.models.RedMatch; + const AccessToken = app.models.AccessToken; const getLastLoginDateTime = () => ctx.options.currentUser ? Rx.bindNodeCallback(AccessToken.find.bind(AccessToken))({ - where: { - userId: ctx.data.redUserId, - }, - order: ['created DESC'], - limit: 1, - }).pipe( - map((accessTokens) => - accessTokens && accessTokens[0] ? accessTokens[0].created : null - ) + where: { + userId: ctx.data.redUserId, + }, + order: ['created DESC'], + limit: 1, + }).pipe( + map((accessTokens) => + accessTokens?.[0] ? accessTokens[0].created : null ) - : Rx.of([null]) + ) + : Rx.of([null]); const countMatchesByType = (type) => Rx.bindNodeCallback(RedMatch.count.bind(RedMatch))({ mentorId: ctx.data.id, status: type, - }) - const countTotal = () => countMatchesByType(null) + }); + const countTotal = () => countMatchesByType(null); Rx.zip(getLastLoginDateTime(), countTotal()).subscribe( ([lastLoginDateTime, totalRedMatchCount]) => { Object.assign(ctx.data, { lastLoginDateTime, totalRedMatchCount, - }) - next() + }); + next(); }, (err) => next(err) - ) - }) + ); + }); RedProfile.observe('loaded', (ctx, next) => { if (ctx.isNewInstance) { - return next() + return next(); // TODO: the next two else-if blocks can definitely be DRY-ed. Merge them. } - if (!ctx.data.categories) ctx.data.categories = [] + if (!ctx.data.categories) ctx.data.categories = []; // Strip away RedProfile.administratorInternalComment if user is NOT cloud-accounts@redi-school.org - if ( - ctx.options && - ctx.options.currentUser && - ctx.options.currentUser.email === 'cloud-accounts@redi-school.org' - ) { - } else { - delete ctx.data.administratorInternalComment + if (ctx.options?.currentUser?.email !== 'cloud-accounts@redi-school.org') { + delete ctx.data.administratorInternalComment; //TODO avoid delete } // If favouritedRedProfileIds[] isn't set, set it. But delete it if the accessing user doesn't own it. - ctx.data.favouritedRedProfileIds = ctx.data.favouritedRedProfileIds || [] + ctx.data.favouritedRedProfileIds = ctx.data.favouritedRedProfileIds || []; const currentUserRedProfileId = - ctx?.options?.currentUser?.redProfile?.id.toString() + ctx?.options?.currentUser?.redProfile?.id.toString(); const isRedProfileOwnedByCurrentUser = - currentUserRedProfileId && - currentUserRedProfileId === ctx.data.id.toString() - if (!isRedProfileOwnedByCurrentUser) delete ctx.data.favouritedRedProfileIds + currentUserRedProfileId === ctx.data.id.toString(); + if (!isRedProfileOwnedByCurrentUser) delete ctx.data.favouritedRedProfileIds; //TODO avoid delete - if (ctx.data && ctx.data.userType === 'mentor') { + if (ctx.data?.userType === 'mentor') { // In case RedProfile belongs to a mentor, add "computed properties" // currentMenteeCount, currentFreeMenteeSpots, and numberOfPendingApplicationWithCurrentUser, // currentApplicantCount - const RedMatch = app.models.RedMatch - const RedMentoringSession = app.models.RedMentoringSession + const RedMatch = app.models.RedMatch; + const RedMentoringSession = app.models.RedMentoringSession; const countMatchesByType = (type) => Rx.bindNodeCallback(RedMatch.count.bind(RedMatch))({ mentorId: ctx.data.id, status: type, - }) - const countAcceptedMatches = () => countMatchesByType('accepted') - const countAppliedMatches = () => countMatchesByType('applied') - const countTotal = () => countMatchesByType(undefined) + }); + const countAcceptedMatches = () => countMatchesByType('accepted'); + const countAppliedMatches = () => countMatchesByType('applied'); + const countTotal = () => countMatchesByType(null); const numberOfPendingApplicationWithCurrentUser = () => ctx.options.currentUser ? Rx.bindNodeCallback(RedMatch.count.bind(RedMatch))({ - mentorId: ctx.data.id, - menteeId: ctx.options.currentUser.redProfile.id, - status: 'applied', - }) - : Rx.of([null]) + mentorId: ctx.data.id, + menteeId: ctx.options.currentUser.redProfile.id, + status: 'applied', + }) + : Rx.of([null]); const getRedMatchesToCurrentMentor = () => ctx.options.currentUser ? Rx.bindNodeCallback(RedMatch.find.bind(RedMatch))({ - where: { - menteeId: ctx.options.currentUser.redProfile.id, - mentorId: ctx.data.id, - }, - }) - : Rx.of([null]) + where: { + menteeId: ctx.options.currentUser.redProfile.id, + mentorId: ctx.data.id, + }, + }) + : Rx.of([null]); const getRedMentoringSessionsToCurrentMentor = () => ctx.options.currentUser ? Rx.bindNodeCallback( - RedMentoringSession.find.bind(RedMentoringSession) - )({ - where: { - menteeId: ctx.options.currentUser.redProfile.id, - mentorId: ctx.data.id, - }, - }) - : Rx.of([null]) + RedMentoringSession.find.bind(RedMentoringSession) + )({ + where: { + menteeId: ctx.options.currentUser.redProfile.id, + mentorId: ctx.data.id, + }, + }) + : Rx.of([null]); Rx.zip( countAcceptedMatches(), @@ -197,50 +185,50 @@ module.exports = function (RedProfile) { numberOfPendingApplicationWithCurrentUser, redMatchesWithCurrentUser, redMentoringSessionsWithCurrentUser, - }) - next() + }); + next(); }, (err) => next(err) - ) - } else if (ctx.data && ctx.data.userType === 'mentee') { + ); + } else if (ctx.data?.userType === 'mentee') { // In case RedProfile belongs to a mentee, add "computed properties" // numberOfPendingApplicationWithCurrentUser, - const RedMatch = app.models.RedMatch - const RedMentoringSession = app.models.RedMentoringSession + const RedMatch = app.models.RedMatch; + const RedMentoringSession = app.models.RedMentoringSession; const countActiveMentorMatches = (type) => Rx.bindNodeCallback(RedMatch.count.bind(RedMatch))({ menteeId: ctx.data.id, status: 'accepted', - }) + }); const getAllRedMatches = () => Rx.bindNodeCallback(RedMatch.find.bind(RedMatch))({ where: { menteeId: ctx.data.id, }, include: 'mentor', - }) + }); const getRedMatchesToCurrentMentor = () => ctx.options.currentUser ? Rx.bindNodeCallback(RedMatch.find.bind(RedMatch))({ - where: { - menteeId: ctx.data.id, - mentorId: ctx.options.currentUser.redProfile.id, - }, - }) - : Rx.of([null]) + where: { + menteeId: ctx.data.id, + mentorId: ctx.options.currentUser.redProfile.id, + }, + }) + : Rx.of([null]); const getRedMentoringSessionsToCurrentMentor = () => ctx.options.currentUser ? Rx.bindNodeCallback( - RedMentoringSession.find.bind(RedMentoringSession) - )({ - where: { - menteeId: ctx.data.id, - mentorId: ctx.options.currentUser.redProfile.id, - }, - }) - : Rx.of([null]) + RedMentoringSession.find.bind(RedMentoringSession) + )({ + where: { + menteeId: ctx.data.id, + mentorId: ctx.options.currentUser.redProfile.id, + }, + }) + : Rx.of([null]); Rx.zip( countActiveMentorMatches(), @@ -254,66 +242,60 @@ module.exports = function (RedProfile) { redMentoringSessionsWithCurrentUser, allRedMatches, ]) => { - const currentActiveMentors = allRedMatches.filter( - (match) => match.status === 'accepted' - ) + const currentActiveMentors = allRedMatches.filter(({ status }) => + status === 'accepted'); + const currentActiveMentor = - currentActiveMentors.length > 0 + currentActiveMentors.length ? currentActiveMentors[0] - : undefined - const hasActiveMentor = !!currentActiveMentor + : null; + const hasActiveMentor = !!currentActiveMentor; Object.assign(ctx.data, { activeMentorMatchesCount, redMatchesWithCurrentUser, redMentoringSessionsWithCurrentUser, ifUserIsMentee_hasActiveMentor: hasActiveMentor, - ifUserIsMentee_activeMentor: - currentActiveMentor && - currentActiveMentor.toJSON && - currentActiveMentor.toJSON().mentor, - }) - next() + ifUserIsMentee_activeMentor: currentActiveMentor?.toJSON?.().mentor, + }); + next(); }, (err) => next(err) - ) + ); } else { - next() + next(); } - }) + }); RedProfile.pendingReviewDoAccept = function (data, options, callback) { - pendingReviewAcceptOrDecline('ACCEPT')(data, options, callback) - } + pendingReviewAcceptOrDecline('ACCEPT')(data, options, callback); + }; RedProfile.pendingReviewDoDecline = function (data, options, callback) { - pendingReviewAcceptOrDecline('DECLINE')(data, options, callback) - } + pendingReviewAcceptOrDecline('DECLINE')(data, options, callback); + }; const pendingReviewAcceptOrDecline = (acceptDecline) => async (data, options, callback) => { if (!_.includes(['ACCEPT', 'DECLINE'], acceptDecline)) { - throw new Error('Invalid acceptDecline parameter') + throw new Error('Invalid acceptDecline parameter'); } - const { redProfileId } = data + const { redProfileId } = data; const mentorRole = await app.models.Role.findOne({ where: { name: 'mentor' }, - }) + }); const menteeRole = await app.models.Role.findOne({ where: { name: 'mentee' }, - }) + }); const findRedProfile = switchMap(({ redProfileId }) => loopbackModelMethodToObservable(RedProfile, 'findById')(redProfileId) - ) + ); const validateCurrentUserType = switchMap((redProfileInst) => { - const userType = redProfileInst.toJSON().userType + const userType = redProfileInst.toJSON().userType; if (_.includes(pendingReviewTypes, userType)) { - return of(redProfileInst) - } else { - throw new Error( - 'Invalid current userType (user is not pending review)' - ) + return of(redProfileInst); } - }) + throw new Error('Invalid current userType (user is not pending review)'); + }); const setNewRedProfileProperties = switchMap((redProfileInst) => loopbackModelMethodToObservable( redProfileInst, @@ -323,19 +305,19 @@ module.exports = function (RedProfile) { redProfileInst.toJSON().userType ]() ) - ) + ); const createRoleMapping = switchMap((redProfileInst) => { - const { userType, redUserId } = redProfileInst.toJSON() + const { userType, redUserId } = redProfileInst.toJSON(); if (!_.includes(['mentee', 'mentor'], userType)) { - return of(redProfileInst) + return of(redProfileInst); } - const role = userType === 'mentor' ? mentorRole : menteeRole + const role = userType === 'mentor' ? mentorRole : menteeRole; role.principals.create({ principalType: app.models.RoleMapping.USER, principalId: redUserId, - }) - return of(redProfileInst) - }) + }); + return of(redProfileInst); + }); Rx.of({ redProfileId }) .pipe( @@ -345,39 +327,35 @@ module.exports = function (RedProfile) { createRoleMapping, sendEmailUserReviewedAcceptedOrDenied ) - .subscribe( - (redMatchInst) => { - callback(null, redMatchInst) - }, + .subscribe((redMatchInst) => { + callback(null, redMatchInst); + }, (err) => console.log(err) - ) - } + ); + }; RedProfile.observe('after save', async function (context, next) { // Onky continue if this is a brand new user - if (process.env.NODE_ENV === 'seeding') return next() + if (process.env.NODE_ENV === 'seeding') return next(); - const redProfileInst = context.instance - const redUserInst = await redProfileInst.redUser.get() - const redProfile = redProfileInst.toJSON() - const redUser = redUserInst.toJSON() + const redProfileInst = context.instance; + const redUserInst = await redProfileInst.redUser.get(); + const redProfile = redProfileInst.toJSON(); + const redUser = redUserInst.toJSON(); - redUserInst.updateAttribute('rediLocation', redProfile.rediLocation) + redUserInst.updateAttribute('rediLocation', redProfile.rediLocation); - if (!context.isNewInstance) return next() + if (!context.isNewInstance) return next(); // Special case: a new RedProfile is created (by code in red-user.js) if a user already exists - // (RedUser) but they have an existing Tp Jobsekeer user (TpJobseekerProfile). If so, we don't + // (RedUser) but they have an existing Tp Jobsekeer user (TpJobSeekerProfile). If so, we don't // proceed with the below verification email, but shoot off a special email to the user. - if ( - redProfile.signupSource === - 'existing-user-with-tp-profile-logging-into-con' - ) { - sendEmailToUserWithTpJobseekerProfileSigningUpToCon({ + if (redProfile.signupSource === 'existing-user-with-tp-profile-logging-into-con') { + sendEmailToUserWithTpJobSeekerProfileSigningUpToCon({ recipient: redUser.email, firstName: redProfile.firstName, - }).subscribe() - return next() + }).subscribe(); + return next(); } var verifyOptions = { @@ -391,20 +369,20 @@ module.exports = function (RedProfile) { userType: redProfile.userType, verificationToken: verifyOptions.verificationToken, rediLocation: redProfile.rediLocation, - }).subscribe() + }).subscribe(); }, }, to: redUser.email, from: 'dummy@dummy.com', - } + }; redUserInst.verify(verifyOptions, function (err, response) { - console.log(err) - console.log(response) - next() - }) - }) -} + console.log(err); + console.log(response); + next(); + }); + }); +}; /** * Send email to user whose pending status has just been accepted/rejected. @@ -413,30 +391,30 @@ module.exports = function (RedProfile) { */ const sendEmailUserReviewedAcceptedOrDenied = switchMap((redProfileInst) => { - const userType = redProfileInst.toJSON().userType + const userType = redProfileInst.toJSON().userType; const userTypeToEmailMap = { mentor: sendMentorPendingReviewAcceptedEmail, mentee: sendMenteePendingReviewAcceptedEmail, 'public-sign-up-mentor-rejected': sendPendingReviewDeclinedEmail, 'public-sign-up-mentee-rejected': sendPendingReviewDeclinedEmail, - } + }; if (!_.has(userTypeToEmailMap, userType)) { - throw new Error('User does not have valid user type') + throw new Error('User does not have valid user type'); } - const emailFunc = userTypeToEmailMap[userType] - const { contactEmail, firstName, rediLocation } = redProfileInst.toJSON() + const emailFunc = userTypeToEmailMap[userType]; + const { contactEmail, firstName, rediLocation } = redProfileInst.toJSON(); return emailFunc({ recipient: contactEmail, firstName, rediLocation, userType, - }) -}) + }); +}); const pendingReviewTypes = [ 'public-sign-up-mentor-pending-review', 'public-sign-up-mentee-pending-review', -] +]; const currentUserTypeToPostReviewUpdates = { ACCEPT: { 'public-sign-up-mentor-pending-review': () => ({ @@ -462,10 +440,10 @@ const currentUserTypeToPostReviewUpdates = { userActivated: false, }), }, -} +}; const loopbackModelMethodToObservable = (loopbackModel, modelMethod) => (methodParameter) => Rx.bindNodeCallback(loopbackModel[modelMethod].bind(loopbackModel))( methodParameter - ) + ); diff --git a/apps/api/common/models/red-user.js b/apps/api/common/models/red-user.js index 9f26cb062..33f962fca 100644 --- a/apps/api/common/models/red-user.js +++ b/apps/api/common/models/red-user.js @@ -1,40 +1,40 @@ -'use strict' +'use strict'; const { sendResetPasswordEmail, sendMenteeRequestAppointmentEmail, sendMentorRequestAppointmentEmail, -} = require('../../lib/email/email') +} = require('../../lib/email/email'); const { - sendTpJobseekerEmailVerificationSuccessfulEmail, + sendTpJobSeekerEmailVerificationSuccessfulEmail, sendTpCompanyEmailVerificationSuccessfulEmail, sendTpResetPasswordEmail, -} = require('../../lib/email/tp-email') +} = require('../../lib/email/tp-email'); module.exports = function (RedUser) { - RedUser.observe('before save', function updateTimestamp(ctx, next) { + RedUser.observe('before save', function updateTimestamp (ctx, next) { if (ctx.instance) { - if (ctx.isNewInstance) ctx.instance.createdAt = new Date() - ctx.instance.updatedAt = new Date() + if (ctx.isNewInstance) ctx.instance.createdAt = new Date(); + ctx.instance.updatedAt = new Date(); } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); } - next() - }) + next(); + }); RedUser.afterRemote('confirm', async function (ctx, inst, next) { const redUserInst = await RedUser.findById(ctx.args.uid, { - include: ['redProfile', 'tpJobseekerProfile', 'tpCompanyProfile'], - }) - const redUser = redUserInst.toJSON() + include: ['redProfile', 'tpJobSeekerProfile', 'tpCompanyProfile'], + }); + const redUser = redUserInst.toJSON(); - const userSignedUpWithCon = !!redUser.redProfile - const userSignedUpWithTpAndIsJobseeker = !!redUser.tpJobseekerProfile - const userSignedUpWithTpAndIsCompany = !!redUser.tpCompanyProfile + const userSignedUpWithCon = !!redUser.redProfile; + const userSignedUpWithTpAndIsJobSeeker = !!redUser.tpJobSeekerProfile; + const userSignedUpWithTpAndIsCompany = !!redUser.tpCompanyProfile; if (userSignedUpWithCon) { - const userType = redUser.redProfile.userType + const userType = redUser.redProfile.userType; switch (userType) { case 'public-sign-up-mentee-pending-review': @@ -42,83 +42,83 @@ module.exports = function (RedUser) { recipient: redUser.email, firstName: redUser.redProfile.firstName, rediLocation: redUser.redProfile.rediLocation, - }).toPromise() - return + }).toPromise(); + return; case 'public-sign-up-mentor-pending-review': await sendMentorRequestAppointmentEmail({ recipient: redUser.email, firstName: redUser.redProfile.firstName, rediLocation: redUser.redProfile.rediLocation, - }).toPromise() - return + }).toPromise(); + return; default: - throw new Error('Invalid user type') + throw new Error('Invalid user type'); } } - if (userSignedUpWithTpAndIsJobseeker) { - await sendTpJobseekerEmailVerificationSuccessfulEmail({ + if (userSignedUpWithTpAndIsJobSeeker) { + await sendTpJobSeekerEmailVerificationSuccessfulEmail({ recipient: redUser.email, - firstName: redUser.tpJobseekerProfile.firstName, - }).toPromise() + firstName: redUser.tpJobSeekerProfile.firstName, + }).toPromise(); } if (userSignedUpWithTpAndIsCompany) { await sendTpCompanyEmailVerificationSuccessfulEmail({ recipient: redUser.email, firstName: redUser.tpCompanyProfile.firstName, - }).toPromise() + }).toPromise(); } - }) + }); RedUser.requestResetPasswordEmail = function (body, cb) { - const email = body.email - const redproduct = body.redproduct + const email = body.email; + const redproduct = body.redproduct; RedUser.resetPassword( { email, redproduct, }, function (err) { - if (err) return cb(err) - cb(null) + if (err) return cb(err); + cb(null); } - ) - } + ); + }; RedUser.remoteMethod('requestResetPasswordEmail', { accepts: { arg: 'data', type: 'object', http: { source: 'body' } }, returns: { arg: 'resp', type: 'object', root: true }, - }) + }); RedUser.on('resetPasswordRequest', async function (info) { - const accessToken = encodeURIComponent(JSON.stringify(info.accessToken)) - const email = info.user.email - const redproduct = info.options.redproduct + const accessToken = encodeURIComponent(JSON.stringify(info.accessToken)); + const email = info.user.email; + const redproduct = info.options.redproduct; const redUserInst = await RedUser.findById(info.user.id, { - include: ['redProfile', 'tpJobseekerProfile', 'tpCompanyProfile'], - }) - const redUser = redUserInst.toJSON() + include: ['redProfile', 'tpJobSeekerProfile', 'tpCompanyProfile'], + }); + const redUser = redUserInst.toJSON(); - const userSignedUpWithCon = !!redUser.redProfile - const userSignedUpWithTpAndIsJobseeker = !!redUser.tpJobseekerProfile - const userSignedUpWithTpAndIsCompany = !!redUser.tpCompanyProfile + const userSignedUpWithCon = !!redUser.redProfile; + const userSignedUpWithTpAndIsJobSeeker = !!redUser.tpJobSeekerProfile; + const userSignedUpWithTpAndIsCompany = !!redUser.tpCompanyProfile; - let firstName - let rediLocation + let firstName; + let rediLocation; if (userSignedUpWithCon) { - firstName = redUser.redProfile.firstName - rediLocation = redUser.redProfile.rediLocation + firstName = redUser.redProfile.firstName; + rediLocation = redUser.redProfile.rediLocation; } - if (userSignedUpWithTpAndIsJobseeker) { - firstName = redUser.tpJobseekerProfile.firstName + if (userSignedUpWithTpAndIsJobSeeker) { + firstName = redUser.tpJobSeekerProfile.firstName; } if (userSignedUpWithTpAndIsCompany) { - firstName = redUser.tpCompanyProfile.firstName + firstName = redUser.tpCompanyProfile.firstName; } if (redproduct === 'CON') { @@ -127,120 +127,121 @@ module.exports = function (RedUser) { firstName, accessToken, rediLocation, - }).subscribe() + }).subscribe(); } else if (redproduct === 'TP') { sendTpResetPasswordEmail({ recipient: email, firstName, accessToken, - }).subscribe() + rediLocation + }).subscribe(); } - }) + }); /****************** * Special post-login hook: * When a user logs into one of our products (CON and TP), it's possible they don't * have a product profile. For example, a user can have signed up in TP initially - * and created a TP profile (TpJobseekerProfile). Then, they log into CON but don't + * and created a TP profile (TpJobSeekerProfile). Then, they log into CON but don't * have a CON profile (RedProfile) yet. Or vice versa (CON-only user logging into * TP). * This hook detects when one of these two cases occur, and creates the appropriate * product profile. */ RedUser.afterRemote('login', async function (ctx, loginOutput, next) { - const redProduct = ctx.req.headers.redproduct // either CON or TP + const redProduct = ctx.req.headers.redproduct; // either CON or TP switch (redProduct) { case 'CON': - return loginHook_caseLoginIntoConnect(ctx, next) + return loginHook_caseLoginIntoConnect(ctx, next); case 'TP': - return loginHook_caseLoginIntoTalentPool(ctx, next) + return loginHook_caseLoginIntoTalentPool(ctx, next); default: - return next() + return next(); } - }) + }); - async function loginHook_caseLoginIntoConnect(context, next) { - const redUserInst = await loginHook_getRedUser(context) - const redUser = redUserInst.toJSON() + async function loginHook_caseLoginIntoConnect (context, next) { + const redUserInst = await loginHook_getRedUser(context); + const redUser = redUserInst.toJSON(); - const userAlreadyHasConProfile = Boolean(redUser.redProfile) - const userDoesNotHaveTpJobseekerProfile = !Boolean( - redUser.tpJobseekerProfile - ) + const userAlreadyHasConProfile = Boolean(redUser.redProfile); + const userDoesNotHaveTpJobSeekerProfile = !Boolean( + redUser.tpJobSeekerProfile + ); - if (userAlreadyHasConProfile || userDoesNotHaveTpJobseekerProfile) - return next() + if (userAlreadyHasConProfile || userDoesNotHaveTpJobSeekerProfile) + return next(); - const conProfile = tpJobseekerProfileToConRedProfile( - redUser.tpJobseekerProfile - ) + const conProfile = tpJobSeekerProfileToConRedProfile( + redUser.tpJobSeekerProfile + ); - await redUserInst.redProfile.create(conProfile) + await redUserInst.redProfile.create(conProfile); - return next() + return next(); } - async function loginHook_caseLoginIntoTalentPool(context, next) { - const redUserInst = await loginHook_getRedUser(context) - const redUser = redUserInst.toJSON() + async function loginHook_caseLoginIntoTalentPool (context, next) { + const redUserInst = await loginHook_getRedUser(context); + const redUser = redUserInst.toJSON(); - const userAlreadyHasTalentPoolJobseekerProfile = Boolean( - redUser.tpJobseekerProfile - ) - const userDoesNotHaveConnectProfile = !redUser.redProfile + const userAlreadyHasTalentPoolJobSeekerProfile = Boolean( + redUser.tpJobSeekerProfile + ); + const userDoesNotHaveConnectProfile = !redUser.redProfile; if ( - userAlreadyHasTalentPoolJobseekerProfile || + userAlreadyHasTalentPoolJobSeekerProfile || userDoesNotHaveConnectProfile ) { - return next() + return next(); } - const tpJobseekerProfile = conRedProfileToTpJobseekerProfile( + const tpJobSeekerProfile = conRedProfileToTpJobSeekerProfile( redUser.redProfile - ) + ); - await redUserInst.tpJobseekerProfile.create(tpJobseekerProfile) + await redUserInst.tpJobSeekerProfile.create(tpJobSeekerProfile); - return next() + return next(); } - async function loginHook_getRedUser(context) { - const redUserId = context.result.toJSON().userId.toString() + async function loginHook_getRedUser (context) { + const redUserId = context.result.toJSON().userId.toString(); const redUserInst = await RedUser.findById(redUserId, { - include: ['redProfile', 'tpJobseekerProfile'], - }) + include: ['redProfile', 'tpJobSeekerProfile'], + }); - return redUserInst + return redUserInst; } - function conRedProfileToTpJobseekerProfile(profile) { - const tpJobseekerProfile = { + function conRedProfileToTpJobSeekerProfile (redProfile) { + const tpJobSeekerProfile = { firstName: redProfile.firstName, lastName: redProfile.lastName, contactEmail: redProfile.contactEmail, currentlyEnrolledInCourse: redProfile.mentee_currentlyEnrolledInCourse, state: 'drafting-profile', gaveGdprConsentAt: redProfile.gaveGdprConsentAt, - } + }; - return tpJobseekerProfile + return tpJobSeekerProfile; } - function tpJobseekerProfileToConRedProfile(tpJobseekerProfile) { + function tpJobSeekerProfileToConRedProfile (tpJobSeekerProfile) { const conRedProfile = { - firstName: tpJobseekerProfile.firstName, - lastName: tpJobseekerProfile.lastName, - contactEmail: tpJobseekerProfile.contactEmail, - currentlyEnrolledInCourse: tpJobseekerProfile.currentlyEnrolledInCourse, + firstName: tpJobSeekerProfile.firstName, + lastName: tpJobSeekerProfile.lastName, + contactEmail: tpJobSeekerProfile.contactEmail, + currentlyEnrolledInCourse: tpJobSeekerProfile.currentlyEnrolledInCourse, userType: 'public-sign-up-mentee-pending-review', - gaveGdprConsentAt: tpJobseekerProfile.gaveGdprConsentAt, + gaveGdprConsentAt: tpJobSeekerProfile.gaveGdprConsentAt, signupSource: 'existing-user-with-tp-profile-logging-into-con', rediLocation: 'berlin', administratorInternalComment: 'SYSTEM NOTE: This user first signed up in Talent Pool. They then logged into Connect. Their ReDI Location has been set to BERLIN. Make sure to figure out if they should be changed to Munich or NRW. If so, request Eric or Anil to do the change', - } + }; - return conRedProfile + return conRedProfile; } -} +}; diff --git a/apps/api/common/models/red-user.json b/apps/api/common/models/red-user.json index fb922296f..20879fd4f 100644 --- a/apps/api/common/models/red-user.json +++ b/apps/api/common/models/red-user.json @@ -21,17 +21,17 @@ "nestRemoting": true } }, - "tpJobseekerProfile": { + "tpJobSeekerProfile": { "type": "hasOne", - "model": "TpJobseekerProfile", + "model": "TpJobSeekerProfile", "foreignKey": "", "options": { "nestRemoting": true } }, - "tpJobseekerCv": { + "tpJobSeekerCv": { "type": "hasMany", - "model": "TpJobseekerCv", + "model": "TpJobSeekerCv", "foreignKey": "", "options": { "nestRemoting": true @@ -64,16 +64,16 @@ "__create__redProfile", "__update__redProfile", "__get__redProfile", - "__create__tpJobseekerProfile", - "__update__tpJobseekerProfile", - "__get__tpJobseekerProfile", - "__create__tpJobseekerCv", - "__update__tpJobseekerCv", - "__get__tpJobseekerCv", - "__findById__tpJobseekerCv", - "__updateById__tpJobseekerCv", - "__destroyById__tpJobseekerCv", - "__delete__tpJobseekerCv", + "__create__tpJobSeekerProfile", + "__update__tpJobSeekerProfile", + "__get__tpJobSeekerProfile", + "__create__tpJobSeekerCv", + "__update__tpJobSeekerCv", + "__get__tpJobSeekerCv", + "__findById__tpJobSeekerCv", + "__updateById__tpJobSeekerCv", + "__destroyById__tpJobSeekerCv", + "__delete__tpJobSeekerCv", "__create__tpCompanyProfile", "__update__tpCompanyProfile", "__get__tpCompanyProfile", diff --git a/apps/api/common/models/tp-jobfair-2021-interview-match.js b/apps/api/common/models/tp-jobfair-2021-interview-match.js index e223b570c..170dc3c34 100644 --- a/apps/api/common/models/tp-jobfair-2021-interview-match.js +++ b/apps/api/common/models/tp-jobfair-2021-interview-match.js @@ -1,14 +1,14 @@ -'use strict' +'use strict'; -const Rx = require('rxjs') -const { switchMap } = require('rxjs/operators') +const Rx = require('rxjs'); +const { switchMap } = require('rxjs/operators'); -const app = require('../../server/server') +const app = require('../../server/server'); const { sendMentorshipRequestReceivedEmail, sendMentorshipAcceptedEmail, sendNotificationToMentorThatPendingApplicationExpiredSinceOtherMentorAccepted, -} = require('../../lib/email/email') +} = require('../../lib/email/email'); module.exports = function (TpJobfair2021InterviewMatch) { /** @@ -19,20 +19,20 @@ module.exports = function (TpJobfair2021InterviewMatch) { TpJobfair2021InterviewMatch.observe( 'before save', - async function updateTimestamp(ctx, next) { - if (process.env.NODE_ENV === 'seeding') return next() + async function updateTimestamp (ctx, next) { + if (process.env.NODE_ENV === 'seeding') return next(); if (ctx.instance) { if (ctx.isNewInstance) { - ctx.instance.createdAt = new Date() + ctx.instance.createdAt = new Date(); } - ctx.instance.updatedAt = new Date() + ctx.instance.updatedAt = new Date(); } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); } - next() + next(); } - ) + ); // IMPORTANT ACL-related method: this combines with the ACL $authenticated-can-execute-find, // to allow $mentor and $mentee to look up TpJobfair2021InterviewMatch. To make sure a mentee user can only @@ -40,47 +40,42 @@ module.exports = function (TpJobfair2021InterviewMatch) { // is used. TpJobfair2021InterviewMatch.observe( 'access', - function onlyMatchesRelatedToCurrentUser(ctx, next) { - if (!ctx.options.currentUser) return next() + function onlyMatchesRelatedToCurrentUser (ctx, next) { + if (!ctx.options.currentUser) return next(); - const currentUserTpJobseekerProfile = - ctx.options.currentUser.tpJobseekerProfile + const currentUserTpJobSeekerProfile = + ctx.options.currentUser.tpJobSeekerProfile; const currentUserTpCompanyProfile = - ctx.options.currentUser.tpCompanyProfile + ctx.options.currentUser.tpCompanyProfile; // TODO: this one is included (as of writing) only for convenience in admin panel. // Considering putting in a if(adminUser) block - ctx.query.include = ['interviewee', 'company', 'jobListing'] + ctx.query.include = ['interviewee', 'company', 'jobListing']; - if (!ctx.query.where) ctx.query.where = {} + if (!ctx.query.where) ctx.query.where = {}; // TODO: Replace this with role-based 'admin' role check if (ctx.options.currentUser.email !== 'cloud-accounts@redi-school.org') { - const currentUserJobseekerOrCompany = { - or: [], - } - if (currentUserTpJobseekerProfile) { - currentUserJobseekerOrCompany.or.push({ - intervieweeId: currentUserTpJobseekerProfile.id, - }) + const currentUserJobSeekerOrCompany = { or: [] }; + + if (currentUserTpJobSeekerProfile) { + currentUserJobSeekerOrCompany.or.push({ + intervieweeId: currentUserTpJobSeekerProfile.id, + }); } if (currentUserTpCompanyProfile) { - currentUserJobseekerOrCompany.or.push({ + currentUserJobSeekerOrCompany.or.push({ companyId: currentUserTpCompanyProfile.id, - }) + }); } - const existingWhere = ctx.query.where - if (Object.values(existingWhere).length > 0) { - ctx.query.where = { - and: [currentUserJobseekerOrCompany, existingWhere], - } - } else { - ctx.query.where = currentUserJobseekerOrCompany - } + const existingWhere = ctx.query.where; + ctx.query.where = Object.values(existingWhere).length + ? { and: [currentUserJobSeekerOrCompany, existingWhere] } + : currentUserJobSeekerOrCompany; } - next() + next(); } - ) -} + ); +}; diff --git a/apps/api/common/models/tp-jobfair-2021-interview-match.json b/apps/api/common/models/tp-jobfair-2021-interview-match.json index 2fabcd13d..47aad3f84 100644 --- a/apps/api/common/models/tp-jobfair-2021-interview-match.json +++ b/apps/api/common/models/tp-jobfair-2021-interview-match.json @@ -15,7 +15,7 @@ "relations": { "interviewee": { "type": "belongsTo", - "model": "TpJobseekerProfile", + "model": "TpJobSeekerProfile", "foreignKey": "", "options": { "nestRemoting": true diff --git a/apps/api/common/models/tp-jobseeker-cv.js b/apps/api/common/models/tp-jobseeker-cv.js index ef1c38359..2df2debb3 100644 --- a/apps/api/common/models/tp-jobseeker-cv.js +++ b/apps/api/common/models/tp-jobseeker-cv.js @@ -1,57 +1,52 @@ -'use strict' -const _ = require('lodash') +'use strict'; +const _ = require('lodash'); -const Rx = require('rxjs') -const { of } = Rx -const { switchMap, map } = require('rxjs/operators') +const Rx = require('rxjs'); +const { of } = Rx; +const { switchMap, map } = require('rxjs/operators'); -const app = require('../../server/server') +const app = require('../../server/server'); const addFullNamePropertyForAdminSearch = (ctx) => { - let thingToUpdate - if (ctx.instance) thingToUpdate = ctx.instance - else if (ctx.data) thingToUpdate = ctx.data - else return + let thingToUpdate; + if (ctx.instance) thingToUpdate = ctx.instance; + else if (ctx.data) thingToUpdate = ctx.data; + else return; - const firstName = thingToUpdate.firstName - const lastName = thingToUpdate.lastName + const firstName = thingToUpdate.firstName; + const lastName = thingToUpdate.lastName; if (firstName || lastName) { - const merged = `${firstName ? firstName + ' ' : ''}${lastName || ''}` - thingToUpdate.loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName = merged + const merged = `${firstName ? firstName + ' ' : ''}${lastName || ''}`; + thingToUpdate.loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName = merged; } -} +}; -module.exports = function (TpJobseekerCv) { - TpJobseekerCv.observe('before save', function updateTimestamp(ctx, next) { - addFullNamePropertyForAdminSearch(ctx) - const currentDate = new Date() +module.exports = function (TpJobSeekerCv) { + TpJobSeekerCv.observe('before save', function updateTimestamp (ctx, next) { + addFullNamePropertyForAdminSearch(ctx); + const currentDate = new Date(); if (ctx.instance) { if (ctx.isNewInstance) { - ctx.instance.createdAt = currentDate + ctx.instance.createdAt = currentDate; } - ctx.instance.updatedAt = new Date() + ctx.instance.updatedAt = new Date(); } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); } - next() - }) + next(); + }); - TpJobseekerCv.observe('loaded', (ctx, next) => { + TpJobSeekerCv.observe('loaded', (ctx, next) => { if (ctx.isNewInstance) { - return next() + return next(); // TODO: the next two else-if blocks can definitely be DRY-ed. Merge them. } - if ( - ctx.options && - ctx.options.currentUser && - ctx.options.currentUser.email === 'cloud-accounts@redi-school.org' - ) { - } else { - delete ctx.data.administratorInternalComment + if (ctx.options?.currentUser?.email !== 'cloud-accounts@redi-school.org') { + delete ctx.data.administratorInternalComment; //TODO avoid delete } - next() - }) -} + next(); + }); +}; diff --git a/apps/api/common/models/tp-jobseeker-cv.json b/apps/api/common/models/tp-jobseeker-cv.json index 7dfb832cf..6031b565c 100644 --- a/apps/api/common/models/tp-jobseeker-cv.json +++ b/apps/api/common/models/tp-jobseeker-cv.json @@ -1,6 +1,6 @@ { - "name": "TpJobseekerCv", - "plural": "tpJobseekerCvs", + "name": "TpJobSeekerCv", + "plural": "tpJobSeekerCvs", "base": "PersistedModel", "idInjection": true, "options": { diff --git a/apps/api/common/models/tp-jobseeker-profile.js b/apps/api/common/models/tp-jobseeker-profile.js index 4036091f1..ab4072150 100644 --- a/apps/api/common/models/tp-jobseeker-profile.js +++ b/apps/api/common/models/tp-jobseeker-profile.js @@ -1,57 +1,57 @@ -'use strict' -const _ = require('lodash') +'use strict'; +const _ = require('lodash'); -const Rx = require('rxjs') -const { of } = Rx -const { switchMap, map } = require('rxjs/operators') +const Rx = require('rxjs'); +const { of } = Rx; +const { switchMap, map } = require('rxjs/operators'); -const app = require('../../server/server') +const app = require('../../server/server'); const { - sendTpJobseekerVerificationEmail, - sendTpJobseekerjobseekerProfileApprovedInstructToSubmitJobPreferencesEmail, - sendTpJobseekerjobseekerProfileNotApprovedYet, -} = require('../../lib/email/tp-email') + sendTpJobSeekerVerificationEmail, + sendTpJobSeekerjobSeekerProfileApprovedInstructToSubmitJobPreferencesEmail, + sendTpJobSeekerjobSeekerProfileNotApprovedYet, +} = require('../../lib/email/tp-email'); const addFullNamePropertyForAdminSearch = (ctx) => { - let thingToUpdate - if (ctx.instance) thingToUpdate = ctx.instance - else if (ctx.data) thingToUpdate = ctx.data - else return + let thingToUpdate; + if (ctx.instance) thingToUpdate = ctx.instance; + else if (ctx.data) thingToUpdate = ctx.data; + else return; - const firstName = thingToUpdate.firstName - const lastName = thingToUpdate.lastName + const firstName = thingToUpdate.firstName; + const lastName = thingToUpdate.lastName; if (firstName || lastName) { - const merged = `${firstName ? firstName + ' ' : ''}${lastName || ''}` + const merged = `${firstName ? firstName + ' ' : ''}${lastName || ''}`; thingToUpdate.loopbackComputedDoNotSetElsewhere__forAdminSearch__fullName = - merged + merged; } -} +}; -module.exports = function (TpJobseekerProfile) { - TpJobseekerProfile.observe( +module.exports = function (TpJobSeekerProfile) { + TpJobSeekerProfile.observe( 'before save', - function updateTimestamp(ctx, next) { - addFullNamePropertyForAdminSearch(ctx) - const currentDate = new Date() + function updateTimestamp (ctx, next) { + addFullNamePropertyForAdminSearch(ctx); + const currentDate = new Date(); if (ctx.instance) { if (ctx.isNewInstance) { - ctx.instance.createdAt = currentDate - ctx.instance.gaveGdprConsentAt = currentDate + ctx.instance.createdAt = currentDate; + ctx.instance.gaveGdprConsentAt = currentDate; } - ctx.instance.updatedAt = new Date() + ctx.instance.updatedAt = new Date(); } else { - ctx.data.updatedAt = new Date() + ctx.data.updatedAt = new Date(); } - next() + next(); } - ) + ); - TpJobseekerProfile.observe( + TpJobSeekerProfile.observe( 'loaded', - function getLastLoginDateTime(ctx, next) { + function getLastLoginDateTime (ctx, next) { if (ctx.isNewInstance) { - return next() + return next(); // TODO: the next two else-if blocks can definitely be DRY-ed. Merge them. } @@ -61,41 +61,41 @@ module.exports = function (TpJobseekerProfile) { ctx.options.currentUser.email === 'cloud-accounts@redi-school.org' ) { } else { - return next() + return next(); } - const AccessToken = app.models.AccessToken + const AccessToken = app.models.AccessToken; const getLastLoginDateTime = () => ctx.options.currentUser ? Rx.bindNodeCallback(AccessToken.find.bind(AccessToken))({ - where: { - userId: ctx.data.redUserId, - }, - order: ['created DESC'], - limit: 1, - }).pipe( - map((accessTokens) => - accessTokens && accessTokens[0] ? accessTokens[0].created : null - ) + where: { + userId: ctx.data.redUserId, + }, + order: ['created DESC'], + limit: 1, + }).pipe( + map((accessTokens) => + accessTokens && accessTokens[0] ? accessTokens[0].created : null ) - : Rx.of([null]) + ) + : Rx.of([null]); getLastLoginDateTime().subscribe( (lastLoginDateTime) => { Object.assign(ctx.data, { lastLoginDateTime, - }) - next() + }); + next(); }, (err) => next(err) - ) + ); } - ) + ); - TpJobseekerProfile.observe('loaded', (ctx, next) => { + TpJobSeekerProfile.observe('loaded', (ctx, next) => { if (ctx.isNewInstance) { - return next() + return next(); // TODO: the next two else-if blocks can definitely be DRY-ed. Merge them. } @@ -105,144 +105,144 @@ module.exports = function (TpJobseekerProfile) { ctx.options.currentUser.email === 'cloud-accounts@redi-school.org' ) { } else { - delete ctx.data.administratorInternalComment + delete ctx.data.administratorInternalComment; } - next() - }) + next(); + }); - TpJobseekerProfile.pendingReviewDoAccept = function ( + TpJobSeekerProfile.pendingReviewDoAccept = function ( data, options, callback ) { - pendingReviewAcceptOrDecline('ACCEPT')(data, options, callback) - } + pendingReviewAcceptOrDecline('ACCEPT')(data, options, callback); + }; - TpJobseekerProfile.pendingReviewDoDecline = function ( + TpJobSeekerProfile.pendingReviewDoDecline = function ( data, options, callback ) { - pendingReviewAcceptOrDecline('DECLINE')(data, options, callback) - } + pendingReviewAcceptOrDecline('DECLINE')(data, options, callback); + }; const pendingReviewAcceptOrDecline = (acceptDecline) => async (data, options, callback) => { if (!_.includes(['ACCEPT', 'DECLINE'], acceptDecline)) { - throw new Error('Invalid acceptDecline parameter') + throw new Error('Invalid acceptDecline parameter'); } - const { tpJobseekerProfileId } = data - const jobseekerRole = await app.models.Role.findOne({ - where: { name: 'jobseeker' }, - }) - const findTpJobseekerProfile = switchMap(({ tpJobseekerProfileId }) => + const { tpJobSeekerProfileId } = data; + const jobSeekerRole = await app.models.Role.findOne({ + where: { name: 'jobSeeker' }, + }); + const findTpJobSeekerProfile = switchMap(({ tpJobSeekerProfileId }) => loopbackModelMethodToObservable( - TpJobseekerProfile, + TpJobSeekerProfile, 'findById' - )(tpJobseekerProfileId) - ) - const validateCurrentState = switchMap((tpJobseekerProfileInst) => { - const state = tpJobseekerProfileInst.toJSON().state + )(tpJobSeekerProfileId) + ); + const validateCurrentState = switchMap((tpJobSeekerProfileInst) => { + const state = tpJobSeekerProfileInst.toJSON().state; if (state === 'submitted-for-review') { - return of(tpJobseekerProfileInst) + return of(tpJobSeekerProfileInst); } else { throw new Error( 'Invalid current state (is not "submitted-for-review")' - ) + ); } - }) - const setNewTpJobseekerProfileProperties = switchMap( - (tpJobseekerProfileInst) => + }); + const setNewTpJobSeekerProfileProperties = switchMap( + (tpJobSeekerProfileInst) => loopbackModelMethodToObservable( - tpJobseekerProfileInst, + tpJobSeekerProfileInst, 'updateAttributes' )( currentUserStateToPostReviewUpdates[acceptDecline][ - tpJobseekerProfileInst.toJSON().state + tpJobSeekerProfileInst.toJSON().state ] ) - ) - const createRoleMapping = switchMap((tpJobseekerProfileInst) => { - const { redUserId } = tpJobseekerProfileInst.toJSON() - jobseekerRole.principals.create({ + ); + const createRoleMapping = switchMap((tpJobSeekerProfileInst) => { + const { redUserId } = tpJobSeekerProfileInst.toJSON(); + jobSeekerRole.principals.create({ principalType: app.models.RoleMapping.USER, principalId: redUserId, - }) - return of(tpJobseekerProfileInst) - }) + }); + return of(tpJobSeekerProfileInst); + }); const sendEmailUserReviewedAcceptedOrDenied = switchMap( - (tpJobseekerProfileInst) => { - const { contactEmail, firstName } = tpJobseekerProfileInst.toJSON() + (tpJobSeekerProfileInst) => { + const { contactEmail, firstName } = tpJobSeekerProfileInst.toJSON(); const stateToEmailFuncMap = { ACCEPT: - sendTpJobseekerjobseekerProfileApprovedInstructToSubmitJobPreferencesEmail, - DECLINE: sendTpJobseekerjobseekerProfileNotApprovedYet, - } - const emailFunc = stateToEmailFuncMap[acceptDecline] + sendTpJobSeekerjobSeekerProfileApprovedInstructToSubmitJobPreferencesEmail, + DECLINE: sendTpJobSeekerjobSeekerProfileNotApprovedYet, + }; + const emailFunc = stateToEmailFuncMap[acceptDecline]; return emailFunc({ recipient: contactEmail, firstName, - }) + }); } - ) + ); - Rx.of({ tpJobseekerProfileId }) + Rx.of({ tpJobSeekerProfileId }) .pipe( - findTpJobseekerProfile, + findTpJobSeekerProfile, validateCurrentState, - setNewTpJobseekerProfileProperties, + setNewTpJobSeekerProfileProperties, createRoleMapping, sendEmailUserReviewedAcceptedOrDenied ) .subscribe( - (tpJobseekerProfileInst) => { - callback(null, tpJobseekerProfileInst) + (tpJobSeekerProfileInst) => { + callback(null, tpJobSeekerProfileInst); }, (err) => console.log(err) - ) - } + ); + }; - TpJobseekerProfile.observe('after save', async function (context, next) { + TpJobSeekerProfile.observe('after save', async function (context, next) { // Onky continue if this is a brand new user - if (process.env.NODE_ENV === 'seeding') return next() + if (process.env.NODE_ENV === 'seeding') return next(); - const tpJobseekerProfileInst = context.instance - const redUserInst = await tpJobseekerProfileInst.redUser.get() - const tpJobseekerProfile = tpJobseekerProfileInst.toJSON() - const redUser = redUserInst.toJSON() + const tpJobSeekerProfileInst = context.instance; + const redUserInst = await tpJobSeekerProfileInst.redUser.get(); + const tpJobSeekerProfile = tpJobSeekerProfileInst.toJSON(); + const redUser = redUserInst.toJSON(); - redUserInst.updateAttribute('rediLocation', tpJobseekerProfile.rediLocation) + redUserInst.updateAttribute('rediLocation', tpJobSeekerProfile.rediLocation); - if (!context.isNewInstance) return next() + if (!context.isNewInstance) return next(); var verifyOptions = { type: 'email', mailer: { send: async (verifyOptions, context, cb) => { - sendTpJobseekerVerificationEmail({ + sendTpJobSeekerVerificationEmail({ recipient: verifyOptions.to, redUserId: redUser.id, - firstName: tpJobseekerProfile.firstName, + firstName: tpJobSeekerProfile.firstName, verificationToken: verifyOptions.verificationToken, - rediLocation: tpJobseekerProfile.rediLocation, - }).subscribe() + rediLocation: tpJobSeekerProfile.rediLocation, + }).subscribe(); }, }, to: redUser.email, from: 'dummy@dummy.com', - } + }; redUserInst.verify(verifyOptions, function (err, response) { - console.log(err) - console.log(response) - next() - }) - }) + console.log(err); + console.log(response); + next(); + }); + }); - // ensure only administrator can see TpJobseekerCv when isProfileVisibleToCompanies is false - // TpJobseekerProfile.observe( + // ensure only administrator can see TpJobSeekerCv when isProfileVisibleToCompanies is false + // TpJobSeekerProfile.observe( // 'access', // function onlyMatchesRelatedToCurrentUser(ctx, next) { // if (!ctx.query.where) ctx.query.where = {} @@ -272,13 +272,13 @@ module.exports = function (TpJobseekerProfile) { // next() // } // ) -} +}; const loopbackModelMethodToObservable = (loopbackModel, modelMethod) => (methodParameter) => Rx.bindNodeCallback(loopbackModel[modelMethod].bind(loopbackModel))( methodParameter - ) + ); const currentUserStateToPostReviewUpdates = { ACCEPT: { @@ -291,27 +291,27 @@ const currentUserStateToPostReviewUpdates = { state: 'drafting-profile', }, }, -} +}; const sendEmailUserReviewedAcceptedOrDenied = switchMap( - (tpJobseekerProfileInst) => { - const userType = tpJobseekerProfileInst.toJSON().userType + (tpJobSeekerProfileInst) => { + const userType = tpJobSeekerProfileInst.toJSON().userType; const userTypeToEmailMap = { mentor: sendMentorPendingReviewAcceptedEmail, mentee: sendMenteePendingReviewAcceptedEmail, 'public-sign-up-mentor-rejected': sendPendingReviewDeclinedEmail, 'public-sign-up-mentee-rejected': sendPendingReviewDeclinedEmail, - } + }; if (!_.has(userTypeToEmailMap, userType)) { - throw new Error('User does not have valid user type') + throw new Error('User does not have valid user type'); } - const emailFunc = userTypeToEmailMap[userType] - const { contactEmail, firstName, rediLocation } = redProfileInst.toJSON() + const emailFunc = userTypeToEmailMap[userType]; + const { contactEmail, firstName, rediLocation } = redProfileInst.toJSON(); return emailFunc({ recipient: contactEmail, firstName, rediLocation, userType, - }) + }); } -) +); diff --git a/apps/api/common/models/tp-jobseeker-profile.json b/apps/api/common/models/tp-jobseeker-profile.json index 9fdbd7b7d..f9f8a0155 100644 --- a/apps/api/common/models/tp-jobseeker-profile.json +++ b/apps/api/common/models/tp-jobseeker-profile.json @@ -1,6 +1,6 @@ { - "name": "TpJobseekerProfile", - "plural": "tpJobseekerProfiles", + "name": "TpJobSeekerProfile", + "plural": "tpJobSeekerProfiles", "base": "PersistedModel", "idInjection": true, "options": { @@ -88,7 +88,7 @@ ], "returns": [ { - "arg": "tpJobseekerProfile", + "arg": "tpJobSeekerProfile", "type": "object", "root": true, "description": "" @@ -119,7 +119,7 @@ ], "returns": [ { - "arg": "tpJobseekerProfile", + "arg": "tpJobSeekerProfile", "type": "object", "root": true, "description": "" diff --git a/apps/api/lib/email/email.js b/apps/api/lib/email/email.js index c0a30954f..21402c86a 100644 --- a/apps/api/lib/email/email.js +++ b/apps/api/lib/email/email.js @@ -1,39 +1,39 @@ -'use strict' +'use strict'; -const aws = require('aws-sdk') -const Rx = require('rxjs') -const mjml2html = require('mjml') -const nodemailer = require('nodemailer') -const fs = require('fs') -const path = require('path') +const aws = require('aws-sdk'); +const Rx = require('rxjs'); +const mjml2html = require('mjml'); +const nodemailer = require('nodemailer'); +const fs = require('fs'); +const path = require('path'); const config = { accessKeyId: process.env.EMAILER_AWS_ACCESS_KEY, secretAccessKey: process.env.EMAILER_AWS_SECRET_KEY, region: process.env.EMAILER_AWS_REGION, -} -const { buildFrontendUrl } = require('../build-frontend-url') -const { buildBackendUrl } = require('../build-backend-url') +}; +const { buildFrontendUrl } = require('../build-frontend-url'); +const { buildBackendUrl } = require('../build-backend-url'); -const ses = new aws.SES(config) +const ses = new aws.SES(config); const transporter = nodemailer.createTransport({ SES: ses, -}) +}); const isProductionOrDemonstration = () => - ['production', 'demonstration', 'staging'].includes(process.env.NODE_ENV) + ['production', 'demonstration', 'staging'].includes(process.env.NODE_ENV); -const sendEmail = Rx.bindNodeCallback(ses.sendEmail.bind(ses)) +const sendEmail = Rx.bindNodeCallback(ses.sendEmail.bind(ses)); const sendMjmlEmail = Rx.bindNodeCallback( transporter.sendMail.bind(transporter) -) +); const sendEmailFactory = (to, subject, body, rediLocation) => { - let toSanitized = isProductionOrDemonstration() ? to : '' + let toSanitized = isProductionOrDemonstration() ? to : ''; if (process.env.DEV_MODE_EMAIL_RECIPIENT) { - toSanitized = process.env.DEV_MODE_EMAIL_RECIPIENT + toSanitized = process.env.DEV_MODE_EMAIL_RECIPIENT; } - let sender = 'career@redi-school.org' + let sender = 'career@redi-school.org'; return sendEmail({ Source: sender, Destination: { @@ -52,62 +52,62 @@ const sendEmailFactory = (to, subject, body, rediLocation) => { Data: buildSubjectLine(subject, process.env.NODE_ENV), }, }, - }) -} + }); +}; const sendMjmlEmailFactory = ({ to, subject, html }) => { - let toSanitized = isProductionOrDemonstration() ? to : '' + let toSanitized = isProductionOrDemonstration() ? to : ''; if (process.env.DEV_MODE_EMAIL_RECIPIENT) { - toSanitized = process.env.DEV_MODE_EMAIL_RECIPIENT + toSanitized = process.env.DEV_MODE_EMAIL_RECIPIENT; } - let sender = 'career@redi-school.org' + let sender = 'career@redi-school.org'; return sendMjmlEmail({ from: sender, to: toSanitized, bcc: ['career@redi-school.org'], subject: buildSubjectLine(subject, process.env.NODE_ENV), html: html, - }) -} + }); +}; -function buildSubjectLine(subject, env) { +function buildSubjectLine (subject, env) { switch (env) { case 'production': - return subject + return subject; case 'demonstration': - return `[DEMO ENVIRONMENT] ${subject}` + return `[DEMO ENVIRONMENT] ${subject}`; default: - return `[DEV ENVIRONMENT] ${subject}` + return `[DEV ENVIRONMENT] ${subject}`; } } const sendReportProblemEmailTemplate = fs.readFileSync( path.resolve(__dirname, 'templates', 'send-problem-report.mjml'), 'utf-8' -) +); const sendReportProblemEmailParsed = mjml2html(sendReportProblemEmailTemplate, { filePath: path.resolve(__dirname, 'templates'), -}) +}); const sendReportProblemEmail = ({ sendingUserEmail, message }) => { const html = sendReportProblemEmailParsed.html .replace(/\${sendingUserEmail}/g, sendingUserEmail) - .replace(/\${message}/g, message) + .replace(/\${message}/g, message); return sendMjmlEmailFactory({ to: 'career@redi-school.org', subject: 'New problem report', html: html, - }) -} + }); +}; const sendResetPasswordEmailTemplate = fs.readFileSync( path.resolve(__dirname, 'templates', 'reset-password.mjml'), 'utf-8' -) +); const sendResetPasswordEmailParsed = mjml2html(sendResetPasswordEmailTemplate, { filePath: path.resolve(__dirname, 'templates'), -}) +}); const sendResetPasswordEmail = ({ recipient, @@ -118,19 +118,19 @@ const sendResetPasswordEmail = ({ const resetPasswordUrl = `${buildFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/reset-password/set-new-password/${accessToken}` - const rediEmailAdress = 'career@redi-school.org' + )}/front/reset-password/set-new-password/${accessToken}`; + const rediEmailAdress = 'career@redi-school.org'; const html = sendResetPasswordEmailParsed.html .replace(/\${firstName}/g, firstName) .replace(/\${resetPasswordUrl}/g, resetPasswordUrl) .replace(/\${rediEmailAdress}/g, rediEmailAdress) - .replace(/\${emailAdress}/g, recipient) + .replace(/\${emailAdress}/g, recipient); return sendMjmlEmailFactory({ to: recipient, subject: 'Password Reset for ReDI Connect', html: html, - }) -} + }); +}; const sendPendingReviewDeclinedEmail = ({ recipient, @@ -138,58 +138,57 @@ const sendPendingReviewDeclinedEmail = ({ rediLocation, userType, }) => { - const rediEmailAdress = 'career@redi-school.org' + const rediEmailAdress = 'career@redi-school.org'; const sendPendingReviewDeclinedEmailParsed = convertTemplateToHtml( null, `pending-review-declined-email--${userType}` - ) + ); const html = sendPendingReviewDeclinedEmailParsed .replace(/\${firstName}/g, firstName) .replace(/\${rediLocation}/g, rediLocation) - .replace(/\${rediEmailAdress}/g, rediEmailAdress) + .replace(/\${rediEmailAdress}/g, rediEmailAdress); return sendMjmlEmailFactory({ to: recipient, subject: 'ReDI Connect: Your user registration was declined', html: html, - }) -} + }); +}; const convertTemplateToHtml = (rediLocation, templateString) => { const convertTemplate = fs.readFileSync( path.resolve( __dirname, 'templates', - `${templateString}${ - rediLocation ? `.${rediLocation.toLowerCase()}` : '' + `${templateString}${rediLocation ? `.${rediLocation.toLowerCase()}` : '' }.mjml` ), 'utf-8' - ) + ); const parsedTemplate = mjml2html(convertTemplate, { filePath: path.resolve(__dirname, 'templates'), - }) - return parsedTemplate.html -} + }); + return parsedTemplate.html; +}; const sendNotificationToMentorThatPendingApplicationExpiredSinceOtherMentorAccepted = ({ recipient, mentorName, menteeName, rediLocation }) => { - const rediEmailAdress = 'career@redi-school.org' + const rediEmailAdress = 'career@redi-school.org'; const sendMenteePendingReviewAcceptedEmailParsed = convertTemplateToHtml( null, 'expired-notification-application' - ) + ); const html = sendMenteePendingReviewAcceptedEmailParsed .replace(/\${mentorName}/g, mentorName) .replace(/\${menteeName}/g, menteeName) - .replace(/\${rediEmailAdress}/g, rediEmailAdress) + .replace(/\${rediEmailAdress}/g, rediEmailAdress); return sendMjmlEmailFactory({ to: recipient, subject: `${menteeName}’s mentee application to you has expired!`, html: html, - }) - } + }); + }; const sendMenteePendingReviewAcceptedEmail = ({ recipient, @@ -199,22 +198,22 @@ const sendMenteePendingReviewAcceptedEmail = ({ const homePageUrl = `${buildFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/login/` + )}/front/login/`; const sendMenteePendingReviewAcceptedEmailParsed = convertTemplateToHtml( null, 'welcome-to-redi-mentee' - ) + ); const html = sendMenteePendingReviewAcceptedEmailParsed .replace(/\${firstName}/g, firstName) .replace(/\${mentorOrMentee}/g, 'mentee') .replace(/\${mentorOrMenteeOpposite}/g, 'mentor') - .replace(/\${homePageUrl}/g, homePageUrl) + .replace(/\${homePageUrl}/g, homePageUrl); return sendMjmlEmailFactory({ to: recipient, subject: 'Your ReDI Connect profile is now activated!', html: html, - }) -} + }); +}; const sendMentorPendingReviewAcceptedEmail = ({ recipient, @@ -224,22 +223,22 @@ const sendMentorPendingReviewAcceptedEmail = ({ const homePageUrl = `${buildFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/login/` + )}/front/login/`; const sendMentorPendingReviewAcceptedEmailParsed = convertTemplateToHtml( rediLocation, 'welcome-to-redi-mentor' - ) + ); const html = sendMentorPendingReviewAcceptedEmailParsed .replace(/\${firstName}/g, firstName) .replace(/\${mentorOrMentee}/g, 'mentor') .replace(/\${mentorOrMenteeOpposite}/g, 'mentee') - .replace(/\${homePageUrl}/g, homePageUrl) + .replace(/\${homePageUrl}/g, homePageUrl); return sendMjmlEmailFactory({ to: recipient, subject: 'Your ReDI Connect profile is now activated!', html: html, - }) -} + }); +}; const sendMenteeRequestAppointmentEmail = ({ recipient, @@ -249,17 +248,17 @@ const sendMenteeRequestAppointmentEmail = ({ const sendMenteeRequestAppointmentEmailParsed = convertTemplateToHtml( null, 'validate-email-address-successful-mentee' - ) + ); const html = sendMenteeRequestAppointmentEmailParsed.replace( /\${firstName}/g, firstName - ) + ); return sendMjmlEmailFactory({ to: recipient, subject: 'Your email has been verified for ReDI Connect', html: html, - }) -} + }); +}; const sendMentorRequestAppointmentEmail = ({ recipient, @@ -269,34 +268,34 @@ const sendMentorRequestAppointmentEmail = ({ const sendMenteeRequestAppointmentEmailParsed = convertTemplateToHtml( null, 'validate-email-address-successful-mentor' - ) + ); const html = sendMenteeRequestAppointmentEmailParsed.replace( /\${firstName}/g, firstName - ) + ); return sendMjmlEmailFactory({ to: recipient, subject: 'Your email has been verified for ReDI Connect', html: html, - }) -} + }); +}; -const sendEmailToUserWithTpJobseekerProfileSigningUpToCon = ({ +const sendEmailToUserWithTpJobSeekerProfileSigningUpToCon = ({ recipient, firstName, }) => { - console.log(recipient, firstName) + console.log(recipient, firstName); const emailParsed = convertTemplateToHtml( null, - `schedule-onboarding-call-for-tp-jobseeker-signed-up-as-mentee` - ) - const html = emailParsed.replace(/\${firstName}/g, firstName) + `schedule-onboarding-call-for-tp-jobSeeker-signed-up-as-mentee` + ); + const html = emailParsed.replace(/\${firstName}/g, firstName); return sendMjmlEmailFactory({ to: recipient, subject: 'Welcome to ReDI Connect!', html: html, - }) -} + }); +}; const sendVerificationEmail = ({ recipient, @@ -309,30 +308,30 @@ const sendVerificationEmail = ({ const userType = { 'public-sign-up-mentor-pending-review': 'mentor', 'public-sign-up-mentee-pending-review': 'mentee', - }[signupType] + }[signupType]; const verificationSuccessPageUrl = `${buildFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/signup-complete/${signupType}` + )}/front/signup-complete/${signupType}`; const verificationUrl = `${buildBackendUrl( process.env.NODE_ENV )}/api/redUsers/confirm?uid=${redUserId}&token=${verificationToken}&redirect=${encodeURI( verificationSuccessPageUrl - )}` + )}`; const sendMenteeRequestAppointmentEmailParsed = convertTemplateToHtml( null, `validate-email-address-${userType}` - ) + ); const html = sendMenteeRequestAppointmentEmailParsed .replace(/\${firstName}/g, firstName) .replace(/\${mentorOrMentee}/g, userType) - .replace(/\${verificationUrl}/g, verificationUrl) + .replace(/\${verificationUrl}/g, verificationUrl); return sendMjmlEmailFactory({ to: recipient, subject: 'Verify your email address!', html: html, - }) -} + }); +}; const sendMentoringSessionLoggedEmail = ({ recipient, @@ -343,15 +342,15 @@ const sendMentoringSessionLoggedEmail = ({ const loginUrl = `${buildFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/login` + )}/front/login`; const sendMentoringSessionLoggedEmailParsed = convertTemplateToHtml( null, 'mentoring-session-logged-email' - ) + ); const html = sendMentoringSessionLoggedEmailParsed .replace(/\${mentorName}/g, mentorName) .replace(/\${menteeFirstName}/g, menteeFirstName) - .replace(/\${loginUrl}/g, loginUrl) + .replace(/\${loginUrl}/g, loginUrl); return sendMjmlEmailFactory({ to: recipient, subject: @@ -361,8 +360,8 @@ const sendMentoringSessionLoggedEmail = ({ ), html: html, rediLocation, - }) -} + }); +}; const sendMenteeReminderToApplyToMentorEmail = ({ recipient, @@ -371,17 +370,17 @@ const sendMenteeReminderToApplyToMentorEmail = ({ const sendMenteeReminderToApplyToMentorEmailParsed = convertTemplateToHtml( null, 'apply-to-mentor-reminder-for-mentee' - ) + ); const html = sendMenteeReminderToApplyToMentorEmailParsed.replace( /\${menteeFirstName}/g, menteeFirstName - ) + ); return sendMjmlEmailFactory({ to: recipient, subject: 'Have you checked out or amazing mentors yet?', html: html, - }) -} + }); +}; const sendMentorCancelledMentorshipNotificationEmail = ({ recipient, @@ -389,17 +388,17 @@ const sendMentorCancelledMentorshipNotificationEmail = ({ rediLocation, }) => { const sendMentorCancelledMentorshipNotificationEmailParsed = - convertTemplateToHtml(null, 'mentorship-cancelation-email-mentee') + convertTemplateToHtml(null, 'mentorship-cancelation-email-mentee'); const html = sendMentorCancelledMentorshipNotificationEmailParsed.replace( /\${firstName}/g, firstName - ) + ); return sendMjmlEmailFactory({ to: recipient, subject: 'Your mentor has quit your connection', html: html, - }) -} + }); +}; const sendToMentorConfirmationOfMentorshipCancelled = ({ recipient, @@ -408,16 +407,16 @@ const sendToMentorConfirmationOfMentorshipCancelled = ({ rediLocation, }) => { const sendMentorCancelledMentorshipNotificationEmailParsed = - convertTemplateToHtml(null, 'mentorship-cancelation-email-mentor') + convertTemplateToHtml(null, 'mentorship-cancelation-email-mentor'); const html = sendMentorCancelledMentorshipNotificationEmailParsed .replace(/\${mentorFirstName}/g, mentorFirstName) - .replace(/\${menteeFullName}/g, menteeFullName) + .replace(/\${menteeFullName}/g, menteeFullName); return sendMjmlEmailFactory({ to: recipient, subject: `Your mentorship of ${menteeFullName} has ben cancelled`, html: html, - }) -} + }); +}; const sendMentorshipCompletionEmailToMentor = ({ recipient, @@ -427,16 +426,16 @@ const sendMentorshipCompletionEmailToMentor = ({ const sendMentorshipCompletionEmailToMentorParsed = convertTemplateToHtml( null, 'complete-mentorship-for-mentor' - ) + ); const html = sendMentorshipCompletionEmailToMentorParsed .replace(/\${mentorFirstName}/g, mentorFirstName) - .replace(/\${menteeFirstName}/g, menteeFirstName) + .replace(/\${menteeFirstName}/g, menteeFirstName); return sendMjmlEmailFactory({ to: recipient, subject: `Your mentorship with ${menteeFirstName} is completed!`, html: html, - }) -} + }); +}; const sendMentorshipCompletionEmailToMentee = ({ recipient, @@ -446,16 +445,16 @@ const sendMentorshipCompletionEmailToMentee = ({ const sendMentorshipCompletionEmailToMenteeParsed = convertTemplateToHtml( null, 'complete-mentorship-for-mentee' - ) + ); const html = sendMentorshipCompletionEmailToMenteeParsed .replace(/\${mentorFirstName}/g, mentorFirstName) - .replace(/\${menteeFirstName}/g, menteeFirstName) + .replace(/\${menteeFirstName}/g, menteeFirstName); return sendMjmlEmailFactory({ to: recipient, subject: `Your mentorship with ${mentorFirstName} is completed!`, html: html, - }) -} + }); +}; const sendMentorshipRequestReceivedEmail = ({ recipient, @@ -467,11 +466,11 @@ const sendMentorshipRequestReceivedEmail = ({ const loginUrl = `${buildFrontendUrl( 'production', mentorRediLocation - )}/front/login` + )}/front/login`; const sendMentorshipRequestReceivedEmailParsed = convertTemplateToHtml( null, 'mentorship-request-email' - ) + ); const html = sendMentorshipRequestReceivedEmailParsed .replace( /\${locationNameFormatted}/g, @@ -479,13 +478,13 @@ const sendMentorshipRequestReceivedEmail = ({ ) .replace(/\${mentorName}/g, mentorName) .replace(/\${menteeFullName}/g, menteeFullName) - .replace(/\${loginUrl}/g, loginUrl) + .replace(/\${loginUrl}/g, loginUrl); return sendMjmlEmailFactory({ to: recipient, subject: `You have received an application from ${menteeFullName}!`, html: html, - }) -} + }); +}; const sendMentorshipAcceptedEmail = ({ recipient, @@ -494,22 +493,22 @@ const sendMentorshipAcceptedEmail = ({ mentorReplyMessageOnAccept, rediLocation, }) => { - const rediEmailAdress = 'career@redi-school.org' + const rediEmailAdress = 'career@redi-school.org'; const sendMentorshipAcceptedEmailParsed = convertTemplateToHtml( null, 'mentorship-acceptance-email' - ) + ); const html = sendMentorshipAcceptedEmailParsed .replace(/\${mentorName}/g, mentorName) .replace(/\${menteeName}/g, menteeName) .replace(/\${rediEmailAdress}/g, rediEmailAdress) - .replace(/\${mentorReplyMessageOnAccept}/g, mentorReplyMessageOnAccept) + .replace(/\${mentorReplyMessageOnAccept}/g, mentorReplyMessageOnAccept); return sendMjmlEmailFactory({ to: recipient, subject: `Congratulations! Mentor ${mentorName} has accepted your application, ${menteeName}!`, html: html, - }) -} + }); +}; // TODO: I'm a duplicate of libs/shared-config/src/lib/config.ts, keep me in sync const mentorDeclinesMentorshipReasonForDecliningOptions = [ @@ -523,7 +522,7 @@ const mentorDeclinesMentorshipReasonForDecliningOptions = [ label: 'I think another mentor would be more suitable', }, { id: 'other', label: 'Other' }, -] +]; const sendMentorshipDeclinedEmail = ({ recipient, @@ -535,13 +534,13 @@ const sendMentorshipDeclinedEmail = ({ }) => { let reasonForDecline = mentorDeclinesMentorshipReasonForDecliningOptions.find( (option) => option.id === ifDeclinedByMentor_chosenReasonForDecline - ).label + ).label; if (ifDeclinedByMentor_chosenReasonForDecline === 'other') { ifDeclinedByMentor_chosenReasonForDecline = - ifDeclinedByMentor_ifReasonIsOther_freeText + ifDeclinedByMentor_ifReasonIsOther_freeText; } - const parsed = convertTemplateToHtml(null, 'mentorship-decline-email') + const parsed = convertTemplateToHtml(null, 'mentorship-decline-email'); const html = parsed .replace(/\${mentorName}/g, mentorName) .replace(/\${menteeName}/g, menteeName) @@ -549,7 +548,7 @@ const sendMentorshipDeclinedEmail = ({ .replace( /\${optionalMessageToMentee}/g, ifDeclinedByMentor_optionalMessageToMentee - ) + ); return sendMjmlEmailFactory({ to: recipient, subject: `This time it wasn't a match`.replace( @@ -557,16 +556,16 @@ const sendMentorshipDeclinedEmail = ({ mentorName ), html: html, - }) -} + }); +}; const formatLocationName = (locationIdentifier) => { return { berlin: 'Berlin', munich: 'Munich', nrw: 'NRW', - }[locationIdentifier] -} + }[locationIdentifier]; +}; module.exports = { sendReportProblemEmail, @@ -586,8 +585,8 @@ module.exports = { sendMenteeRequestAppointmentEmail, sendNotificationToMentorThatPendingApplicationExpiredSinceOtherMentorAccepted, sendResetPasswordEmail, - sendEmailToUserWithTpJobseekerProfileSigningUpToCon, + sendEmailToUserWithTpJobSeekerProfileSigningUpToCon, sendVerificationEmail, sendEmailFactory, sendMjmlEmailFactory, -} +}; diff --git a/apps/api/lib/email/tp-email.js b/apps/api/lib/email/tp-email.js index 54ae9bd83..42d028310 100644 --- a/apps/api/lib/email/tp-email.js +++ b/apps/api/lib/email/tp-email.js @@ -1,27 +1,27 @@ -'use strict' +'use strict'; -const aws = require('aws-sdk') -const Rx = require('rxjs') -const mjml2html = require('mjml') -const nodemailer = require('nodemailer') -const fs = require('fs') -const path = require('path') +const aws = require('aws-sdk'); +const Rx = require('rxjs'); +const mjml2html = require('mjml'); +const nodemailer = require('nodemailer'); +const fs = require('fs'); +const path = require('path'); -const { buildTpFrontendUrl } = require('../build-tp-frontend-url') -const { buildBackendUrl } = require('../build-backend-url') +const { buildTpFrontendUrl } = require('../build-tp-frontend-url'); +const { buildBackendUrl } = require('../build-backend-url'); -const { sendMjmlEmailFactory } = require('./email') +const { sendMjmlEmailFactory } = require('./email'); const sendTpResetPasswordEmailTemplate = fs.readFileSync( path.resolve(__dirname, 'tp-templates', 'reset-password.mjml'), 'utf-8' -) +); const sendTpResetPasswordEmailParsed = mjml2html( sendTpResetPasswordEmailTemplate, { filePath: path.resolve(__dirname, 'tp-templates'), } -) +); const sendTpResetPasswordEmail = ({ recipient, @@ -32,38 +32,37 @@ const sendTpResetPasswordEmail = ({ const resetPasswordUrl = `${buildTpFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/reset-password/set-new-password/${accessToken}` - const rediEmailAdress = 'career@redi-school.org' + )}/front/reset-password/set-new-password/${accessToken}`; + const rediEmailAdress = 'career@redi-school.org'; const html = sendTpResetPasswordEmailParsed.html .replace(/\${firstName}/g, firstName) .replace(/\${resetPasswordUrl}/g, resetPasswordUrl) .replace(/\${rediEmailAdress}/g, rediEmailAdress) - .replace(/\${emailAdress}/g, recipient) + .replace(/\${emailAdress}/g, recipient); return sendMjmlEmailFactory({ to: recipient, subject: 'Password Reset for ReDI Talent Pool', html: html, - }) -} + }); +}; const convertTemplateToHtml = (rediLocation, templateString) => { const convertTemplate = fs.readFileSync( path.resolve( __dirname, 'tp-templates', - `${templateString}${ - rediLocation ? `.${rediLocation.toLowerCase()}` : '' + `${templateString}${rediLocation ? `.${rediLocation.toLowerCase()}` : '' }.mjml` ), 'utf-8' - ) + ); const parsedTemplate = mjml2html(convertTemplate, { filePath: path.resolve(__dirname, 'templates'), - }) - return parsedTemplate.html -} + }); + return parsedTemplate.html; +}; -const sendTpJobseekerVerificationEmail = ({ +const sendTpJobSeekerVerificationEmail = ({ recipient, redUserId, firstName, @@ -74,72 +73,72 @@ const sendTpJobseekerVerificationEmail = ({ const verificationSuccessPageUrl = `${buildTpFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/signup-complete/jobseeker` + )}/front/signup-complete/jobSeeker`; const verificationUrl = `${buildBackendUrl( process.env.NODE_ENV )}/api/redUsers/confirm?uid=${redUserId}&token=${verificationToken}&redirect=${encodeURI( verificationSuccessPageUrl - )}` - const sendTpJobseekerVerificationEmailParsed = convertTemplateToHtml( + )}`; + const sendTpJobSeekerVerificationEmailParsed = convertTemplateToHtml( null, - `jobseeker-validate-email-address` - ) - const html = sendTpJobseekerVerificationEmailParsed + `jobSeeker-validate-email-address` + ); + const html = sendTpJobSeekerVerificationEmailParsed .replace(/\${firstName}/g, firstName) - .replace(/\${verificationUrl}/g, verificationUrl) + .replace(/\${verificationUrl}/g, verificationUrl); return sendMjmlEmailFactory({ to: recipient, subject: 'Verify your email address!', html: html, - }) -} + }); +}; -const sendTpJobseekerEmailVerificationSuccessfulEmail = ({ +const sendTpJobSeekerEmailVerificationSuccessfulEmail = ({ recipient, firstName, }) => { - const sendTpJobseekerEmailVerificationSuccessfulEmailParsed = - convertTemplateToHtml(null, 'jobseeker-validate-email-address-successful') - const html = sendTpJobseekerEmailVerificationSuccessfulEmailParsed.replace( + const sendTpJobSeekerEmailVerificationSuccessfulEmailParsed = + convertTemplateToHtml(null, 'jobSeeker-validate-email-address-successful'); + const html = sendTpJobSeekerEmailVerificationSuccessfulEmailParsed.replace( /\${firstName}/g, firstName - ) + ); return sendMjmlEmailFactory({ to: recipient, subject: 'Your email has been verified for Talent Pool', html: html, - }) -} + }); +}; -const sendTpJobseekerjobseekerProfileApprovedInstructToSubmitJobPreferencesEmail = +const sendTpJobSeekerjobSeekerProfileApprovedInstructToSubmitJobPreferencesEmail = ({ recipient, firstName }) => { const emailParsed = convertTemplateToHtml( null, - 'jobseeker-profile-approved-instruct-to-submit-job-preferences' - ) - const html = emailParsed.replace(/\${firstName}/g, firstName) + 'jobSeeker-profile-approved-instruct-to-submit-job-preferences' + ); + const html = emailParsed.replace(/\${firstName}/g, firstName); return sendMjmlEmailFactory({ to: recipient, subject: 'Talent Pool: your profile is approved! ReDI for the next step?', html: html, - }) - } + }); + }; -const sendTpJobseekerjobseekerProfileNotApprovedYet = ({ +const sendTpJobSeekerjobSeekerProfileNotApprovedYet = ({ recipient, firstName, }) => { const emailParsed = convertTemplateToHtml( null, - 'jobseeker-profile-not-approved-yet' - ) - const html = emailParsed.replace(/\${firstName}/g, firstName) + 'jobSeeker-profile-not-approved-yet' + ); + const html = emailParsed.replace(/\${firstName}/g, firstName); return sendMjmlEmailFactory({ to: recipient, subject: 'The approval of your profile is pending', html: html, - }) -} + }); +}; const sendTpCompanyVerificationEmail = ({ recipient, @@ -152,83 +151,83 @@ const sendTpCompanyVerificationEmail = ({ const verificationSuccessPageUrl = `${buildTpFrontendUrl( process.env.NODE_ENV, rediLocation - )}/front/signup-complete/company` + )}/front/signup-complete/company`; const verificationUrl = `${buildBackendUrl( process.env.NODE_ENV )}/api/redUsers/confirm?uid=${redUserId}&token=${verificationToken}&redirect=${encodeURI( verificationSuccessPageUrl - )}` + )}`; const sendTpCompanyVerificationEmailParsed = convertTemplateToHtml( null, `company-validate-email-address` - ) + ); const html = sendTpCompanyVerificationEmailParsed .replace(/\${firstName}/g, firstName) - .replace(/\${verificationUrl}/g, verificationUrl) + .replace(/\${verificationUrl}/g, verificationUrl); return sendMjmlEmailFactory({ to: recipient, subject: 'Verify your email address!', html: html, - }) -} + }); +}; const sendTpCompanyEmailVerificationSuccessfulEmail = ({ recipient, firstName, }) => { - const tpLandingPageUrl = buildTpFrontendUrl(process.env.NODE_ENV) + const tpLandingPageUrl = buildTpFrontendUrl(process.env.NODE_ENV); const sendTpCompanyEmailVerificationSuccessfulEmailParsed = - convertTemplateToHtml(null, 'company-validate-email-address-successful') + convertTemplateToHtml(null, 'company-validate-email-address-successful'); const html = sendTpCompanyEmailVerificationSuccessfulEmailParsed .replace(/\${firstName}/g, firstName) - .replace(/\${tpLandingPageUrl}/g, tpLandingPageUrl) + .replace(/\${tpLandingPageUrl}/g, tpLandingPageUrl); return sendMjmlEmailFactory({ to: recipient, subject: 'Your email has been verified for Talent Pool', html: html, - }) -} + }); +}; const sendTpCompanyProfileApprovedEmail = ({ recipient, firstName }) => { const sendTpCompanyProfileApprovedEmailParsed = convertTemplateToHtml( null, 'company-profile-approved' - ) + ); const html = sendTpCompanyProfileApprovedEmailParsed.replace( /\${firstName}/g, firstName - ) + ); return sendMjmlEmailFactory({ to: recipient, subject: 'Your company profile has been approved for Talent Pool', html: html, - }) -} + }); +}; const sendTpCompanyProfileSubmittedForReviewEmail = ({ companyName }) => { const sendTpCompanyProfileSubmittedForReviewEmailParsed = - convertTemplateToHtml(null, 'company-profile-submitted-for-review') + convertTemplateToHtml(null, 'company-profile-submitted-for-review'); const html = sendTpCompanyProfileSubmittedForReviewEmailParsed.replace( /\${companyName}/g, companyName - ) + ); return sendMjmlEmailFactory({ to: 'birgit@redi-school.org', subject: 'New company in Talent Pool', html, - }) -} + }); +}; module.exports = { sendTpResetPasswordEmail, - sendTpJobseekerVerificationEmail, - sendTpJobseekerEmailVerificationSuccessfulEmail, - sendTpJobseekerjobseekerProfileApprovedInstructToSubmitJobPreferencesEmail, - sendTpJobseekerjobseekerProfileNotApprovedYet, + sendTpJobSeekerVerificationEmail, + sendTpJobSeekerEmailVerificationSuccessfulEmail, + sendTpJobSeekerjobSeekerProfileApprovedInstructToSubmitJobPreferencesEmail, + sendTpJobSeekerjobSeekerProfileNotApprovedYet, sendTpCompanyVerificationEmail, sendTpCompanyEmailVerificationSuccessfulEmail, sendTpCompanyProfileApprovedEmail, sendTpCompanyProfileSubmittedForReviewEmail, -} +}; diff --git a/apps/api/lib/email/tp-templates/company-profile-approved.mjml b/apps/api/lib/email/tp-templates/company-profile-approved.mjml index c60bddcc2..29d9f4e7e 100644 --- a/apps/api/lib/email/tp-templates/company-profile-approved.mjml +++ b/apps/api/lib/email/tp-templates/company-profile-approved.mjml @@ -18,7 +18,7 @@ >Congratulations - your company profile has been approved! Now you are able to browse through our ReDI jobseekers to look for + >Now you are able to browse through our ReDI jobSeekers to look for people who could match the criteria of your job opening. Please feel free to contact them through the email address they provided. You can always add new job listings to your company profile when diff --git a/apps/api/lib/email/tp-templates/jobseeker-profile-approved-instruct-to-submit-job-preferences.mjml b/apps/api/lib/email/tp-templates/jobseeker-profile-approved-instruct-to-submit-job-preferences.mjml index 15a14bd60..48f6111a6 100644 --- a/apps/api/lib/email/tp-templates/jobseeker-profile-approved-instruct-to-submit-job-preferences.mjml +++ b/apps/api/lib/email/tp-templates/jobseeker-profile-approved-instruct-to-submit-job-preferences.mjml @@ -33,7 +33,7 @@ > The companies are also able to browse all jobseekers, see your profile and invite you to an interview, + >The companies are also able to browse all jobSeekers, see your profile and invite you to an interview, if you fit the requirements of any of their open positions. diff --git a/apps/api/scripts/20210210_redi-connect-talent-pool-invitation-all-active-mentors-mentees.js b/apps/api/scripts/20210210_redi-connect-talent-pool-invitation-all-active-mentors-mentees.js index cf0fe59f4..b52919eed 100644 --- a/apps/api/scripts/20210210_redi-connect-talent-pool-invitation-all-active-mentors-mentees.js +++ b/apps/api/scripts/20210210_redi-connect-talent-pool-invitation-all-active-mentors-mentees.js @@ -54,7 +54,7 @@ const emailBodyMjml = newsletterTemplateMjml.replace( ReDI Connect's playmate will be... ReDI Talent Pool! -A new meeting place, this time for jobseekers and companies. Positions will be posted and ReDI jobseekers (many of them ReDI Connect mentees) will apply to them. Jobseekers will also get help to prepare their stunning CVs. +A new meeting place, this time for jobSeekers and companies. Positions will be posted and ReDI jobSeekers (many of them ReDI Connect mentees) will apply to them. JobSeekers will also get help to prepare their stunning CVs. We need some help to get started, and here is where the next surprise comes in: diff --git a/apps/api/scripts/20210301_mentoring-topics-consolidation-data-migration.js b/apps/api/scripts/20210301_mentoring-topics-consolidation-data-migration.js index 2c5fd3fee..f9a6c1dbf 100644 --- a/apps/api/scripts/20210301_mentoring-topics-consolidation-data-migration.js +++ b/apps/api/scripts/20210301_mentoring-topics-consolidation-data-migration.js @@ -12,7 +12,7 @@ const RedProfile = app.models.RedProfile; throw new Error(`Couldn't find mapping for topic ${topicId}`); } const newTopicId = newTopic.mapTo; - if (newTopicId === null) return topicList; + if (!newTopicId) return topicList; if (typeof newTopicId === 'string') return [...topicList, newTopicId]; return [...topicList, ...newTopicId]; }, []); diff --git a/apps/api/scripts/create-roles.js b/apps/api/scripts/create-roles.js index f2eb9482d..3ed0addeb 100644 --- a/apps/api/scripts/create-roles.js +++ b/apps/api/scripts/create-roles.js @@ -1,13 +1,13 @@ -const { bindNodeCallback, from } = require('rxjs') -const { concatMap } = require('rxjs/operators') +const { bindNodeCallback, from } = require('rxjs'); +const { concatMap } = require('rxjs/operators'); -const app = require('../server/server') -const { Role } = app.models +const app = require('../server/server'); +const { Role } = app.models; -const roleCreate = bindNodeCallback(Role.create.bind(Role)) +const roleCreate = bindNodeCallback(Role.create.bind(Role)); -const roles = ['admin', 'mentee', 'mentor', 'jobseeker', 'company'] +const roles = ['admin', 'mentee', 'mentor', 'jobSeeker', 'company']; from(roles) .pipe(concatMap((role) => roleCreate({ name: role }))) - .subscribe(console.log, null, () => process.exit()) + .subscribe(console.log, null, () => process.exit()); diff --git a/apps/api/scripts/seed-database.js b/apps/api/scripts/seed-database.js index c33f99a5f..e8399d0af 100644 --- a/apps/api/scripts/seed-database.js +++ b/apps/api/scripts/seed-database.js @@ -1,9 +1,9 @@ -'use strict' +'use strict'; -const app = require('../server/server.js') -const _ = require('lodash') -const fp = require('lodash/fp') -const Rx = require('rxjs') +const app = require('../server/server.js'); +const _ = require('lodash'); +const fp = require('lodash/fp'); +const Rx = require('rxjs'); const { concatMap, switchMap, @@ -11,8 +11,8 @@ const { tap, toArray, delay, -} = require('rxjs/operators') -const moment = require('moment') +} = require('rxjs/operators'); +const moment = require('moment'); const { RedUser, @@ -22,10 +22,10 @@ const { AccessToken, Role, RoleMapping, -} = app.models +} = app.models; -const personsRaw = require('./random-names.json') -const persons = [] +const personsRaw = require('./random-names.json'); +const persons = []; personsRaw.forEach((person) => { for (let i = 1; i <= 5; i++) { persons.push({ @@ -33,9 +33,9 @@ personsRaw.forEach((person) => { surname: `${person.surname}${i}`, gender: person.gender, region: person.region, - }) + }); } -}) +}); const categories = [ { @@ -170,17 +170,17 @@ const categories = [ }, { id: 'entrepreneurship', label: 'Entrepreneurship', group: 'careerSupport' }, { id: 'freelancing', label: 'Freelancing', group: 'careerSupport' }, -] +]; -const Languages = ['German', 'Arabic', 'Farsi', 'Tigrinya'] +const Languages = ['German', 'Arabic', 'Farsi', 'Tigrinya']; const genders = [ { id: 'male', label: 'Male' }, { id: 'female', label: 'Female' }, { id: 'other', label: 'Other' }, -] +]; -const menteeCountCapacityOptions = [1, 2, 3, 4] +const menteeCountCapacityOptions = [1, 2, 3, 4]; const educationLevels = [ { id: 'middleSchool', label: 'Middle School' }, @@ -189,7 +189,7 @@ const educationLevels = [ { id: 'universityBachelor', label: 'University Degree (Bachelor)' }, { id: 'universityMaster', label: 'University Degree (Master)' }, { id: 'universityPhd', label: 'University Degree (PhD)' }, -] +]; const courses = [ { id: 'basicComputerTraining', label: 'Basic Computer Training' }, @@ -204,25 +204,23 @@ const courses = [ { id: 'blockchainBasics', label: 'Blockchain Basics' }, { id: 'introIosAppsSwift', label: 'Intro to iOS Apps with Swift' }, { id: 'introJava', label: 'Intro to Java' }, -] +]; const menteeOccupationCategories = [ { id: 'job', label: 'Job (full-time/part-time)' }, { id: 'student', label: 'Student (enrolled at university)' }, { id: 'lookingForJob', label: 'Looking for a job' }, { id: 'other', label: 'Other' }, -] -const menteeOccupationCategoriesIds = menteeOccupationCategories.map( - (v) => v.id -) +]; +const menteeOccupationCategoriesIds = menteeOccupationCategories.map(({ id }) => id); const randomString = (charset = 'abcdefghijklmnopqrstuvwxyz', length = 10) => { - let str = '' + let str = ''; for (let i = 0; i < length; i++) { - str += charset[Math.floor(Math.random() * (charset.length - 1))] + str += charset[Math.floor(Math.random() * (charset.length - 1))]; } - return str -} + return str; +}; const pickRandomUserType = () => { const possibleUserTypes = [ @@ -230,18 +228,18 @@ const pickRandomUserType = () => { 'mentee', 'public-sign-up-mentor-pending-review', 'public-sign-up-mentee-pending-review', - ] - const randomIndex = Math.floor(Math.random() * possibleUserTypes.length) - return possibleUserTypes[randomIndex] -} + ]; + const randomIndex = Math.floor(Math.random() * possibleUserTypes.length); + return possibleUserTypes[randomIndex]; +}; const users = fp.compose( fp.take(1000), fp.map(({ name, surname, gender }) => { const rediLocation = - Math.random() > 0.5 ? 'berlin' : Math.random() > 0.5 ? 'munich' : 'nrw' - const email = randomString() + '@' + randomString() + '.com' - const password = email + Math.random() > 0.5 ? 'berlin' : Math.random() > 0.5 ? 'munich' : 'nrw'; + const email = randomString() + '@' + randomString() + '.com'; + const password = email; return { redUser: { email, @@ -262,7 +260,7 @@ const users = fp.compose( mentor_workPlace: randomString(), mentee_occupationCategoryId: menteeOccupationCategoriesIds[ - Math.floor(Math.random() * menteeOccupationCategoriesIds.length) + Math.floor(Math.random() * menteeOccupationCategoriesIds.length) ], mentee_occupationJob_placeOfEmployment: randomString(), mentee_occupationJob_position: randomString(), @@ -276,7 +274,7 @@ const users = fp.compose( 'English' ), otherLanguages: randomString(), - personalDescription: randomString(undefined, 300), + personalDescription: randomString(null, 300), contactEmail: email, slackUsername: randomString(), githubProfileUrl: randomString(), @@ -291,42 +289,42 @@ const users = fp.compose( mentee_currentlyEnrolledInCourse: courses[Math.floor(Math.random() * courses.length)].id, }, - } + }; }) -)(persons) +)(persons); const accessTokenDestroyAll = Rx.bindNodeCallback( AccessToken.destroyAll.bind(AccessToken) -) -const roleDestroyAll = Rx.bindNodeCallback(Role.destroyAll.bind(Role)) +); +const roleDestroyAll = Rx.bindNodeCallback(Role.destroyAll.bind(Role)); const roleMappingDestroyAll = Rx.bindNodeCallback( RoleMapping.destroyAll.bind(RoleMapping) -) +); -const redUserDestroyAll = Rx.bindNodeCallback(RedUser.destroyAll.bind(RedUser)) +const redUserDestroyAll = Rx.bindNodeCallback(RedUser.destroyAll.bind(RedUser)); const redProfileDestroyAll = Rx.bindNodeCallback( RedProfile.destroyAll.bind(RedProfile) -) +); const redMatchDestroyAll = Rx.bindNodeCallback( RedMatch.destroyAll.bind(RedMatch) -) +); const redMentoringSessionDestroyAll = Rx.bindNodeCallback( RedMentoringSession.destroyAll.bind(RedMentoringSession) -) +); const redMatchCreate = (redMatch) => - Rx.bindNodeCallback(RedMatch.create.bind(RedMatch))(redMatch) + Rx.bindNodeCallback(RedMatch.create.bind(RedMatch))(redMatch); const redUserCreate = (redUser) => - Rx.bindNodeCallback(RedUser.create.bind(RedUser))(redUser) + Rx.bindNodeCallback(RedUser.create.bind(RedUser))(redUser); const redProfileCreateOnRedUser = (redUserInst) => (redProfile) => Rx.bindNodeCallback(redUserInst.redProfile.create.bind(redUserInst))( redProfile - ) + ); const ericMenteeRedUser = { password: 'career+testmentee@redi-school.org', email: 'career+testmentee@redi-school.org', -} +}; const ericMenteeRedProfile = { rediLocation: 'berlin', userActivated: true, @@ -362,12 +360,12 @@ const ericMenteeRedProfile = { mentee_highestEducationLevel: 'highSchool', mentee_currentlyEnrolledInCourse: 'salesforceFundamentals', username: 'career+testmentee@redi-school.org', -} +}; const ericMentorRedUser = { password: 'career+testmentor@redi-school.org', email: 'career+testmentor@redi-school.org', -} +}; const ericMentorRedProfile = { rediLocation: 'berlin', userActivated: true, @@ -403,12 +401,12 @@ const ericMentorRedProfile = { categories: categories.map((c) => c.id).filter(() => Math.random() < 0.4), menteeCountCapacity: 2, username: 'career+testmentor@redi-school.org', -} +}; const ericAdminUser = { email: 'cloud-accounts@redi-school.org', password: 'cloud-accounts@redi-school.org', -} +}; const ericAdminRedProfile = { rediLocation: 'berlin', userActivated: true, @@ -444,7 +442,7 @@ const ericAdminRedProfile = { categories: categories.map((c) => c.id).filter(() => Math.random() < 0.4), menteeCountCapacity: 2, username: 'cloud-accounts@redi-school.org', -} +}; Rx.of({}) .pipe( @@ -485,32 +483,32 @@ Rx.of({}) switchMap((data) => { const mentors = data.filter( (userData) => userData.redProfile.userType === 'mentor' - ) + ); const mentees = data.filter( (userData) => userData.redProfile.userType === 'mentee' - ) + ); - let matchesFlat = [] - const locations = ['berlin', 'munich'] + let matchesFlat = []; + const locations = ['berlin', 'munich']; for (let i = 0; i < locations.length; i++) { - const location = locations[i] + const location = locations[i]; const mentorsInLocation = mentors.filter( (data) => data.redProfile.rediLocation === location - ) + ); const menteesInLocation = mentees.filter( (data) => data.redProfile.rediLocation === location - ) - console.log('******************************') - console.log('location', location) - console.log(mentorsInLocation.length) - console.log(menteesInLocation.length) - console.log('******************************') + ); + console.log('******************************'); + console.log('location', location); + console.log(mentorsInLocation.length); + console.log(menteesInLocation.length); + console.log('******************************'); const matches = mentorsInLocation.map((mentor) => { return _.sampleSize( menteesInLocation, Math.floor(Math.random() * 10) ).map((mentee) => { - console.log(location) + console.log(location); return { rediLocation: '' + location + '', applicationText: randomString(), @@ -520,12 +518,12 @@ Rx.of({}) ], mentorId: mentor.redProfileInst.id, menteeId: mentee.redProfileInst.id, - } - }) - }) - matchesFlat = [...matchesFlat, ..._.flatten(matches)] + }; + }); + }); + matchesFlat = [...matchesFlat, ..._.flatten(matches)]; } - return Rx.from(matchesFlat) + return Rx.from(matchesFlat); }), concatMap(redMatchCreate) ) @@ -533,10 +531,10 @@ Rx.of({}) () => console.log('next'), console.log, () => { - console.log('done') - process.exit() + console.log('done'); + process.exit(); } - ) + ); -app.models.RedUser.destroyAll() -app.models.RedProfile.destroyAll() +app.models.RedUser.destroyAll(); +app.models.RedProfile.destroyAll(); diff --git a/apps/api/scripts/send-single-reminder-email.js b/apps/api/scripts/send-single-reminder-email.js index b7cf3719c..ef2aee4dd 100644 --- a/apps/api/scripts/send-single-reminder-email.js +++ b/apps/api/scripts/send-single-reminder-email.js @@ -1,8 +1,8 @@ -'use strict' +'use strict'; -const app = require('../server/server.js') -const Rx = require('rxjs') -const { bindNodeCallback, from } = Rx +const app = require('../server/server.js'); +const Rx = require('rxjs'); +const { bindNodeCallback, from } = Rx; const { mergeMap, switchMap, @@ -10,54 +10,53 @@ const { map, tap, count -} = require('rxjs/operators') +} = require('rxjs/operators'); const { sendMentorSignupReminderEmail, sendMenteeSignupReminderEmail -} = require('../lib/email/email') +} = require('../lib/email/email'); const { RedUser -} = app.models +} = app.models; -const redUserFind = q => bindNodeCallback(RedUser.find.bind(RedUser))(q) -const ONE_MONTH = 60 * 60 * 24 * 30 +const redUserFind = q => bindNodeCallback(RedUser.find.bind(RedUser))(q); +const ONE_MONTH = 60 * 60 * 24 * 30; const accessTokenCreateOnRedUser = redUserInst => Rx.bindNodeCallback(redUserInst.createAccessToken.bind(redUserInst))( ONE_MONTH - ) + ); redUserFind({ include: 'redProfile', where: { id: '5cdd5560718d7958e7103b99' } }) .pipe( - switchMap(users => from(users)), - map(data => { + switchMap((users) => from(users)), + map((data) => { return { redUser: data.toJSON(), redProfile: data.toJSON().redProfile, redUserInst: data, redProfileInst: data.redProfile - } + }; }), - filter(data => !data.redProfile.userActivated), + filter(({ redProfile }) => !redProfile.userActivated), mergeMap( data => accessTokenCreateOnRedUser(data.redUserInst), (data, accessToken) => ({ ...data, accessToken }) ), tap(console.log), - mergeMap( - userData => - userData.redProfile.userType === 'mentor' - ? sendMentorSignupReminderEmail( - userData.redProfile.contactEmail, - userData.redProfile.firstName, - encodeURIComponent(JSON.stringify(userData.accessToken.toJSON())) - ) - : sendMenteeSignupReminderEmail( - userData.redProfile.contactEmail, - userData.redProfile.firstName, - encodeURIComponent(JSON.stringify(userData.accessToken.toJSON())) - ), + mergeMap((userData) => + userData.redProfile.userType === 'mentor' + ? sendMentorSignupReminderEmail( + userData.redProfile.contactEmail, + userData.redProfile.firstName, + encodeURIComponent(JSON.stringify(userData.accessToken.toJSON())) + ) + : sendMenteeSignupReminderEmail( + userData.redProfile.contactEmail, + userData.redProfile.firstName, + encodeURIComponent(JSON.stringify(userData.accessToken.toJSON())) + ), (userData, sendResult) => ({ ...userData, sendResult }) ), count() @@ -66,7 +65,7 @@ redUserFind({ include: 'redProfile', where: { id: '5cdd5560718d7958e7103b99' } } count => console.log('did this ' + count + ' times'), e => console.log('Error: ', e), () => console.log('done') - ) + ); /* mergeMap( diff --git a/apps/api/server/model-config.json b/apps/api/server/model-config.json index 19e5384b0..0c4cc046c 100644 --- a/apps/api/server/model-config.json +++ b/apps/api/server/model-config.json @@ -56,11 +56,11 @@ "dataSource": "mongodb", "public": true }, - "TpJobseekerProfile": { + "TpJobSeekerProfile": { "dataSource": "mongodb", "public": true }, - "TpJobseekerCv": { + "TpJobSeekerCv": { "dataSource": "mongodb", "public": true }, diff --git a/apps/api/server/server.js b/apps/api/server/server.js index d072ee673..84f8b8d65 100644 --- a/apps/api/server/server.js +++ b/apps/api/server/server.js @@ -1,58 +1,58 @@ -'use strict' -const path = require('path') +'use strict'; +const path = require('path'); const res = require('dotenv').config({ path: path.resolve(__dirname, '..', '.env.' + process.env.NODE_ENV), -}) +}); // required to set up logger -require('../lib/logger') +require('../lib/logger'); -var loopback = require('loopback') -var boot = require('loopback-boot') +var loopback = require('loopback'); +var boot = require('loopback-boot'); -var http = require('http') -var https = require('https') -var sslConfig = require('./ssl-config') +var http = require('http'); +var https = require('https'); +var sslConfig = require('./ssl-config'); -const Rx = require('rxjs') -const { bindNodeCallback, from } = Rx -const { delay, switchMap, tap, map, filter, count } = require('rxjs/operators') +const Rx = require('rxjs'); +const { bindNodeCallback, from } = Rx; +const { delay, switchMap, tap, map, filter, count } = require('rxjs/operators'); -const { sendMenteeReminderToApplyToMentorEmail } = require('../lib/email/email') -const { default: clsx } = require('clsx') +const { sendMenteeReminderToApplyToMentorEmail } = require('../lib/email/email'); +const { default: clsx } = require('clsx'); -var app = (module.exports = loopback()) +var app = (module.exports = loopback()); -const sendAllReminderEmails = require('../daily-cronjob-reminder-email/index.js') +const sendAllReminderEmails = require('../daily-cronjob-reminder-email/index.js'); app.start = function () { // start the web server return app.listen(function () { - app.emit('started') - var baseUrl = app.get('url').replace(/\/$/, '') - console.log('Web server listening at: %s', baseUrl) + app.emit('started'); + var baseUrl = app.get('url').replace(/\/$/, ''); + console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { - var explorerPath = app.get('loopback-component-explorer').mountPath - console.log('Browse your REST API at %s%s', baseUrl, explorerPath) + var explorerPath = app.get('loopback-component-explorer').mountPath; + console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } - }) -} + }); +}; app.get( '/secret-endpoint-that-will-be-contacted-by-autocode-to-trigger-automated-reminder-emails', (req, res) => { const secretToken = - process.env.DAILY_CRONJOB_SEND_REMINDER_EMAIL_SECRET_TOKEN - const authToken = req.query['auth-token'] - const isDryRun = !!req.query['dry-run'] + process.env.DAILY_CRONJOB_SEND_REMINDER_EMAIL_SECRET_TOKEN; + const authToken = req.query['auth-token']; + const isDryRun = !!req.query['dry-run']; if (authToken === secretToken) { - sendAllReminderEmails(isDryRun) - return res.send({ result: 'email-jobs-queued' }) + sendAllReminderEmails(isDryRun); + return res.send({ result: 'email-jobs-queued' }); } - return res.send({ result: 'nope, wrong pass bro' }) + return res.send({ result: 'nope, wrong pass bro' }); } -) +); // Inject current user into context app @@ -60,19 +60,19 @@ app .phases.addBefore('invoke', 'options-from-request') .use(function (ctx, next) { if (!ctx.args || !ctx.args.options || !ctx.args.options.accessToken) { - return next() + return next(); } - const RedUser = app.models.RedUser + const RedUser = app.models.RedUser; RedUser.findById( ctx.args.options.accessToken.userId, - { include: ['redProfile', 'tpJobseekerProfile', 'tpCompanyProfile'] }, + { include: ['redProfile', 'tpJobSeekerProfile', 'tpCompanyProfile'] }, function (err, user) { - if (err) return next(err) - ctx.args.options.currentUser = user.toJSON() - next() + if (err) return next(err); + ctx.args.options.currentUser = user.toJSON(); + next(); } - ) - }) + ); + }); app.use( '/s3', @@ -84,14 +84,14 @@ app.use( ACL: 'public-read', // this is default uniquePrefix: true, // (4.0.2 and above) default is true, setting the attribute to false preserves the original filename in S3 }) -) +); // Bootstrap the application, configure models, datasources and middleware. // Sub-apps like REST API are mounted via boot scripts. -boot(app, __dirname) +boot(app, __dirname); app.start = function () { - const server = (function buildHttpOrHttpsServer() { + const server = (function buildHttpOrHttpsServer () { if (process.env.USE_HTTPS) { return https.createServer( { @@ -99,28 +99,28 @@ app.start = function () { cert: sslConfig.certificate, }, app - ) + ); } else { - return http.createServer(app) + return http.createServer(app); } - })() + })(); server.listen(app.get('port'), function () { var baseUrl = (process.env.USE_HTTPS ? 'https://' : 'http://') + app.get('host') + ':' + - app.get('port') - app.emit('started', baseUrl) - console.log('LoopBack server listening @ %s%s', baseUrl, '/') + app.get('port'); + app.emit('started', baseUrl); + console.log('LoopBack server listening @ %s%s', baseUrl, '/'); if (app.get('loopback-component-explorer')) { - var explorerPath = app.get('loopback-component-explorer').mountPath - console.log('Browse your REST API at %s%s', baseUrl, explorerPath) + var explorerPath = app.get('loopback-component-explorer').mountPath; + console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } - }) - return server -} + }); + return server; +}; // start the server if `$ node server.js` if (require.main === module) { - app.start() + app.start(); } diff --git a/apps/redi-connect/src/App.tsx b/apps/redi-connect/src/App.tsx index 52c81adcc..19c942eaf 100755 --- a/apps/redi-connect/src/App.tsx +++ b/apps/redi-connect/src/App.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, Suspense } from 'react' +import { useEffect, Suspense } from 'react' import { Provider as StoreProvider } from 'react-redux' import { history, Router } from './services/history/history' @@ -12,7 +12,7 @@ import LocationPicker from './pages/front/landing/LocationPicker' import { Route } from 'react-router-dom' import { QueryParamProvider } from 'use-query-params' -const App = () => { +function App () { switch (envRediLocation()) { case 'location-picker': return ( @@ -26,7 +26,7 @@ const App = () => { } } -const NormalRediConnect = () => { +function NormalRediConnect () { useEffect(() => { store.dispatch(profileFetchStart()) }, []) diff --git a/apps/redi-connect/src/components/AppNotification.tsx b/apps/redi-connect/src/components/AppNotification.tsx index 4cd74f648..c70dc4f57 100644 --- a/apps/redi-connect/src/components/AppNotification.tsx +++ b/apps/redi-connect/src/components/AppNotification.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react' +import { useState, useEffect, FC } from 'react' import { Snackbar, Slide, @@ -58,7 +58,7 @@ const useNotificationStyles = makeStyles((theme: Theme) => ({ export interface AppNotificationOptions { variant: NotificationVariant - autoHideDuration: number | undefined + autoHideDuration: number | null } interface SubjectShowNotification extends AppNotificationOptions { message: string | null @@ -99,7 +99,7 @@ export function AppNotification() { const styleClasses = useNotificationStyles() - const Icon = state ? variantIcon[state.variant] : null + const Icon = variantIcon[state?.variant] || null return ( )} - {state && state.message} + {state?.message} } action={[ diff --git a/apps/redi-connect/src/components/PrivateRoute.tsx b/apps/redi-connect/src/components/PrivateRoute.tsx index 2062555b9..d275a3925 100644 --- a/apps/redi-connect/src/components/PrivateRoute.tsx +++ b/apps/redi-connect/src/components/PrivateRoute.tsx @@ -1,8 +1,7 @@ -import React, { FunctionComponent } from 'react' import { Route, RouteProps } from 'react-router-dom' import { useNotAuthenticatedRedirector } from '../hooks/useNotAuthenticatedRedirector' -export const PrivateRoute: FunctionComponent = (props) => { +export function PrivateRoute (props: RouteProps) { const { isRedirectingToLogin } = useNotAuthenticatedRedirector() // If the hook determined user not authenticated it'll take care of redirect to diff --git a/apps/redi-connect/src/components/Routes.tsx b/apps/redi-connect/src/components/Routes.tsx index 8d37951a8..1c7e309b4 100644 --- a/apps/redi-connect/src/components/Routes.tsx +++ b/apps/redi-connect/src/components/Routes.tsx @@ -1,25 +1,27 @@ -import React from 'react' import { Route, Switch, Redirect } from 'react-router-dom' import { allRoutes } from '../routes/index' import { PrivateRoute } from './PrivateRoute' -export const Routes = () => ( -
- - {allRoutes.map(({ requiresLoggedIn, exact, path, component }, i) => - requiresLoggedIn ? ( - - ) : ( - - ) - )} - - -
-) +export function Routes () { + return ( +
+ + {allRoutes.map(({ requiresLoggedIn, exact, path, component }, i) => + requiresLoggedIn ? ( + + ) : ( + + ) + )} + + +
+ ); +} diff --git a/apps/redi-connect/src/components/molecules/ReadAbout.tsx b/apps/redi-connect/src/components/molecules/ReadAbout.tsx index 2c8f0f82f..e0e793744 100644 --- a/apps/redi-connect/src/components/molecules/ReadAbout.tsx +++ b/apps/redi-connect/src/components/molecules/ReadAbout.tsx @@ -1,53 +1,52 @@ -import React from 'react' import { Content } from 'react-bulma-components' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' + import { Caption, Placeholder, } from '@talent-connect/shared-atomic-design-components' import { RedProfile } from '@talent-connect/shared-types' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile } -const Me = ({ profile }: Props) => { - const { personalDescription, expectations } = profile - - if (!personalDescription && !expectations) { +function Me ({ profile: { personalDescription, expectations }}: Props) { + if (!personalDescription && !expectations) return Please tell us a bit about yourself - } return ( - {personalDescription &&

{personalDescription}

} - {expectations &&

{expectations}

} + {personalDescription && +

{personalDescription}

} + {expectations && +

{expectations}

}
) } -const Some = ({ profile }: Props) => { - const { firstName, lastName, personalDescription, expectations } = profile - +function Some ({ + profile: { firstName, lastName, personalDescription, expectations } +}: Props) { return ( <> About {firstName} {lastName} - {personalDescription &&

{personalDescription}

} - {expectations &&

{expectations}

} + {personalDescription && +

{personalDescription}

} + {expectations && +

{expectations}

}
) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(Me), - Some: ({ profile }: Props) => , + /** */ + Some: (props: Props) => , } diff --git a/apps/redi-connect/src/components/molecules/ReadContactDetails.tsx b/apps/redi-connect/src/components/molecules/ReadContactDetails.tsx index fd46aad6d..ab2b21955 100644 --- a/apps/redi-connect/src/components/molecules/ReadContactDetails.tsx +++ b/apps/redi-connect/src/components/molecules/ReadContactDetails.tsx @@ -1,41 +1,38 @@ -import React from 'react' import { Content } from 'react-bulma-components' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' + import { Caption } from '@talent-connect/shared-atomic-design-components' import { RedProfile } from '@talent-connect/shared-types' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile shortInfo?: boolean } -const ReadContactDetails = ({ profile, shortInfo }: Props) => { - const { firstName, lastName, contactEmail, telephoneNumber } = profile - +function ReadContactDetails ({ + profile: { firstName, lastName, contactEmail, telephoneNumber }, + shortInfo +}: Props) { return ( <> - {shortInfo && Contact Details} + {shortInfo && + Contact Details} - {contactEmail &&

{contactEmail}

} - {!shortInfo && (firstName || lastName) && ( -

- {firstName} {lastName} -

- )} - {telephoneNumber &&

{telephoneNumber}

} + {contactEmail && +

{contactEmail}

} + {!shortInfo && (firstName || lastName) && +

{firstName} {lastName}

} + {telephoneNumber && +

{telephoneNumber}

}
) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(ReadContactDetails), - Some: ({ profile }: Props) => ( - - ), + /** */ + Some: (props: Props) => , } diff --git a/apps/redi-connect/src/components/molecules/ReadEducation.tsx b/apps/redi-connect/src/components/molecules/ReadEducation.tsx index 251b14da8..5cede12d5 100644 --- a/apps/redi-connect/src/components/molecules/ReadEducation.tsx +++ b/apps/redi-connect/src/components/molecules/ReadEducation.tsx @@ -1,33 +1,34 @@ -import React from 'react' import { Content } from 'react-bulma-components' -import { RedProfile } from '@talent-connect/shared-types' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' + +import { RedProfile } from '@talent-connect/shared-types' import { Caption, Placeholder, } from '@talent-connect/shared-atomic-design-components' import { EDUCATION_LEVELS } from '@talent-connect/shared-config' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile shortInfo?: boolean } -const ReadEducation = ({ profile, shortInfo }: Props) => { - const { mentee_highestEducationLevel } = profile - - if (!mentee_highestEducationLevel) { +function ReadEducation ({ + profile: { mentee_highestEducationLevel }, + shortInfo +}: Props) { + if (!mentee_highestEducationLevel) return ( Input your information about your Education here. ) - } return ( <> - {shortInfo && Highest Education} + {shortInfo && + Highest Education}

{EDUCATION_LEVELS[mentee_highestEducationLevel]}

@@ -35,11 +36,7 @@ const ReadEducation = ({ profile, shortInfo }: Props) => { ) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { Me: connect(mapStateToProps, {})(ReadEducation), - Some: ({ profile }: Props) => , + Some: (props: Props) => , } diff --git a/apps/redi-connect/src/components/molecules/ReadLanguages.tsx b/apps/redi-connect/src/components/molecules/ReadLanguages.tsx index ad581fa18..61cff6b08 100644 --- a/apps/redi-connect/src/components/molecules/ReadLanguages.tsx +++ b/apps/redi-connect/src/components/molecules/ReadLanguages.tsx @@ -1,42 +1,39 @@ -import React from 'react' -import { RedProfile } from '@talent-connect/shared-types' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' + +import { RedProfile } from '@talent-connect/shared-types' import { Caption, Placeholder, PipeList, } from '@talent-connect/shared-atomic-design-components' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile } -const Me = ({ profile }: Props) => { - const { languages } = profile +// TODO: language check due to previous wrong typing? +function Me ({ profile: { languages } }: Props) { if (!languages) return Input languages you speak here. return } -const Some = ({ profile }: Props) => { - const { languages } = profile - +function Some ({ profile: { languages } }: Props) { return ( <> Languages - {languages && } + {languages && + } ) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(Me), - Some: ({ profile }: Props) => , + /** */ + Some: (props: Props) => , } diff --git a/apps/redi-connect/src/components/molecules/ReadMenteeCount.tsx b/apps/redi-connect/src/components/molecules/ReadMenteeCount.tsx index 9a0a49e3c..b53030946 100644 --- a/apps/redi-connect/src/components/molecules/ReadMenteeCount.tsx +++ b/apps/redi-connect/src/components/molecules/ReadMenteeCount.tsx @@ -1,44 +1,37 @@ -import React from 'react' import { Content } from 'react-bulma-components' -import { RedProfile } from '@talent-connect/shared-types' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' + +import { RedProfile } from '@talent-connect/shared-types' import { REDI_LOCATION_NAMES } from '@talent-connect/shared-config' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile } -const Me = ({ profile }: Props) => { - const { - menteeCountCapacity, - optOutOfMenteesFromOtherRediLocation, - rediLocation, - } = profile +function Me ({ + profile: { menteeCountCapacity, optOutOfMenteesFromOtherRediLocation, rediLocation } +}: Props) { return ( - {menteeCountCapacity &&

{menteeCountCapacity}

} + {menteeCountCapacity && +

{menteeCountCapacity}

} {!optOutOfMenteesFromOtherRediLocation && (

Let mentees in my location ({REDI_LOCATION_NAMES[rediLocation]}) AND other locations apply for mentorship -

- )} +

)} {optOutOfMenteesFromOtherRediLocation && (

Only let mentees from my own location ( {REDI_LOCATION_NAMES[rediLocation]}) apply for mentorship -

- )} +

)}
) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(Me), } diff --git a/apps/redi-connect/src/components/molecules/ReadMentoringTopics.tsx b/apps/redi-connect/src/components/molecules/ReadMentoringTopics.tsx index b5aa3ca55..2e6bc213f 100644 --- a/apps/redi-connect/src/components/molecules/ReadMentoringTopics.tsx +++ b/apps/redi-connect/src/components/molecules/ReadMentoringTopics.tsx @@ -1,53 +1,52 @@ -import React from 'react' -import { Tag } from 'react-bulma-components' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' + import { Caption, CardTags, Placeholder, - CardTagsProps, } from '@talent-connect/shared-atomic-design-components' + import { CATEGORIES_MAP } from '@talent-connect/shared-config' import { RedProfile } from '@talent-connect/shared-types' +import { CardTagsProps } from 'libs/shared-atomic-design-components/src/lib/atoms/CardTags/CardTags.props'; +import { mapStateToProps } from '../../helpers'; interface ReadMentoringProps { profile: RedProfile caption?: boolean } -export const ProfileTags = ({ items, shortList }: CardTagsProps) => ( - CATEGORIES_MAP[item]} - /> -) +export function ProfileTags (props: CardTagsProps) { + return ( + CATEGORIES_MAP[item]} + /> + ); +} -const ReadMentoringTopics = ({ profile, caption }: ReadMentoringProps) => { - const { categories } = profile +function ReadMentoringTopics ({ + profile: { categories }, + caption = false +}: ReadMentoringProps) { if (!categories?.length && !caption) return Please pick up to four mentoring topics. return ( <> - {caption && {'Mentoring Topics'}} + {caption && + {'Mentoring Topics'}} ) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(ReadMentoringTopics), - Some: ({ profile }: ReadMentoringProps) => ( - - ), - Tags: ({ items, shortList }: CardTagsProps) => ( - - ), + /** */ + Some: (props: ReadMentoringProps) => , + /** */ + Tags: (props: CardTagsProps) => , } diff --git a/apps/redi-connect/src/components/molecules/ReadOccupation.tsx b/apps/redi-connect/src/components/molecules/ReadOccupation.tsx index 4b263b2dc..5d44752a2 100644 --- a/apps/redi-connect/src/components/molecules/ReadOccupation.tsx +++ b/apps/redi-connect/src/components/molecules/ReadOccupation.tsx @@ -1,26 +1,23 @@ -import React from 'react' import { Content } from 'react-bulma-components' import { RedProfile } from '@talent-connect/shared-types' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' import { Caption, Placeholder, } from '@talent-connect/shared-atomic-design-components' import { MENTEE_OCCUPATION_CATEGORY } from '@talent-connect/shared-config' -import { objectEntries } from '@talent-connect/typescript-utilities' +import { mapOptionsObject } from '@talent-connect/typescript-utilities' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile shortInfo?: boolean } -const formMenteeOccupationCategories = objectEntries( - MENTEE_OCCUPATION_CATEGORY -).map(([value, label]) => ({ value, label })) +const formMenteeOccupationCategories = mapOptionsObject(MENTEE_OCCUPATION_CATEGORY) -const ReadOccupation = ({ profile, shortInfo }: Props) => { - const { +function ReadOccupation ({ + profile: { userType, mentor_occupation, mentor_workPlace, @@ -31,7 +28,9 @@ const ReadOccupation = ({ profile, shortInfo }: Props) => { mentee_occupationStudent_studyName, mentee_occupationLookingForJob_what, mentee_occupationOther_description, - } = profile + }, + shortInfo = false +}: Props) { if (!mentor_occupation && !mentee_occupationCategoryId) { return ( @@ -54,40 +53,35 @@ const ReadOccupation = ({ profile, shortInfo }: Props) => { <>

{mentor_occupation}

{mentor_workPlace}

- - )} + )} {isMentee && ( <>

{formMenteeOccupationCategories - .filter((level) => level.value === mentee_occupationCategoryId) - .map((level) => level.label)} + .filter(({ value }) => value === mentee_occupationCategoryId) + .map(({ label }) => label)}

{mentee_occupationCategoryId === 'job' && ( <>

{mentee_occupationJob_placeOfEmployment}

{mentee_occupationJob_position}

- - )} + )} {mentee_occupationCategoryId === 'student' && ( <>

{mentee_occupationStudent_studyPlace}

{mentee_occupationStudent_studyName}

- - )} + )} {mentee_occupationCategoryId === 'lookingForJob' && ( <>

{mentee_occupationLookingForJob_what}

- - )} + )} {mentee_occupationCategoryId === 'other' && ( <>

{mentee_occupationOther_description}

- - )} + )} )} @@ -95,11 +89,9 @@ const ReadOccupation = ({ profile, shortInfo }: Props) => { ) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(ReadOccupation), - Some: ({ profile }: Props) => , + /** */ + Some: (props: Props) => , } diff --git a/apps/redi-connect/src/components/molecules/ReadPersonalDetail.tsx b/apps/redi-connect/src/components/molecules/ReadPersonalDetail.tsx index 0a8cf6666..b9533ceb7 100644 --- a/apps/redi-connect/src/components/molecules/ReadPersonalDetail.tsx +++ b/apps/redi-connect/src/components/molecules/ReadPersonalDetail.tsx @@ -1,23 +1,25 @@ -import React from 'react' import moment from 'moment' -import { RedProfile } from '@talent-connect/shared-types' import { connect } from 'react-redux' -import { RootState } from '../../redux/types' + +import { RedProfile } from '@talent-connect/shared-types' import { Caption, Placeholder, PipeList, } from '@talent-connect/shared-atomic-design-components' import { GENDERS } from '@talent-connect/shared-config' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile caption?: boolean } -const ReadPersonalDetail = ({ profile, caption }: Props) => { - const { gender, birthDate } = profile - +function ReadPersonalDetail ({ + profile: { gender, birthDate }, + caption = false +}: Props) { + const age = moment().diff(birthDate, 'years') const detailsList: string[] = gender ? [GENDERS[gender]] : [] @@ -28,19 +30,16 @@ const ReadPersonalDetail = ({ profile, caption }: Props) => { return ( <> - {caption && Personal Details} + {caption && + Personal Details} ) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(ReadPersonalDetail), - Some: ({ profile }: Props) => ( - - ), + /** */ + Some: ({ profile }: Props) => , } diff --git a/apps/redi-connect/src/components/molecules/ReadRediClass.tsx b/apps/redi-connect/src/components/molecules/ReadRediClass.tsx index a943e4fcd..cbff738af 100644 --- a/apps/redi-connect/src/components/molecules/ReadRediClass.tsx +++ b/apps/redi-connect/src/components/molecules/ReadRediClass.tsx @@ -1,39 +1,34 @@ -import React from 'react' +import { connect } from 'react-redux' + import { Content } from 'react-bulma-components' import { RedProfile } from '@talent-connect/shared-types' -import { connect } from 'react-redux' -import { RootState } from '../../redux/types' import { Caption } from '@talent-connect/shared-atomic-design-components' import { COURSES } from '@talent-connect/shared-config' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile shortInfo?: boolean } -const ReadRediClass = ({ profile, shortInfo }: Props) => { - const { mentee_currentlyEnrolledInCourse } = profile - - const COURSES_MAP = Object.fromEntries( - COURSES.map((course) => [course.id, course.label]) - ) +const COURSES_MAP = Object.fromEntries(COURSES.map(({ id, label }) => [id, label])) +function ReadRediClass ({ + profile: { mentee_currentlyEnrolledInCourse }, + shortInfo = false, +}: Props) { return ( <> - {shortInfo && Redi Class} + {shortInfo && + Redi Class} - {mentee_currentlyEnrolledInCourse && ( -

{COURSES_MAP[mentee_currentlyEnrolledInCourse]}

- )} + {mentee_currentlyEnrolledInCourse && +

{COURSES_MAP[mentee_currentlyEnrolledInCourse]}

}
) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { Me: connect(mapStateToProps, {})(ReadRediClass), Some: ({ profile }: Props) => , diff --git a/apps/redi-connect/src/components/molecules/ReadSocialMedia.tsx b/apps/redi-connect/src/components/molecules/ReadSocialMedia.tsx index 3689cec67..22fc7b9f1 100644 --- a/apps/redi-connect/src/components/molecules/ReadSocialMedia.tsx +++ b/apps/redi-connect/src/components/molecules/ReadSocialMedia.tsx @@ -1,20 +1,22 @@ -import React from 'react' +import { connect } from 'react-redux' + import { Content } from 'react-bulma-components' import { RedProfile } from '@talent-connect/shared-types' -import { connect } from 'react-redux' -import { RootState } from '../../redux/types' import { Caption, Placeholder, } from '@talent-connect/shared-atomic-design-components' +import { mapStateToProps } from '../../helpers'; interface Props { profile: RedProfile shortInfo?: boolean } -const ReadSocialMedia = ({ profile, shortInfo }: Props) => { - const { linkedInProfileUrl, githubProfileUrl, slackUsername } = profile +function ReadSocialMedia ({ + profile: { linkedInProfileUrl, githubProfileUrl, slackUsername }, + shortInfo = false +}: Props) { if ( !shortInfo && @@ -27,7 +29,8 @@ const ReadSocialMedia = ({ profile, shortInfo }: Props) => { return ( <> - {shortInfo && Social Media} + {shortInfo && + Social Media} {linkedInProfileUrl && (

@@ -38,8 +41,7 @@ const ReadSocialMedia = ({ profile, shortInfo }: Props) => { > {linkedInProfileUrl} -

- )} +

)} {githubProfileUrl && (

{ > {githubProfileUrl} -

- )} - {slackUsername &&

{slackUsername}

} +

)} + {slackUsername && +

{slackUsername}

}
) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - export default { + /** */ Me: connect(mapStateToProps, {})(ReadSocialMedia), - Some: ({ profile }: Props) => , + /** */ + Some: (props: Props) => , } diff --git a/apps/redi-connect/src/components/molecules/Teaser.tsx b/apps/redi-connect/src/components/molecules/Teaser.tsx index b3e9de9d4..ddd3aec61 100644 --- a/apps/redi-connect/src/components/molecules/Teaser.tsx +++ b/apps/redi-connect/src/components/molecules/Teaser.tsx @@ -1,4 +1,3 @@ -import React from 'react' import { ReactComponent as WelcomeIllustration } from '../../assets/images/welcome-user.svg' import { ReactComponent as MiriamSvg } from '../../assets/images/miriam.svg' import { ReactComponent as PaulinaSvg } from '../../assets/images/paulina.svg' @@ -17,18 +16,20 @@ type RouteParams = { const miriamStyles = { padding: '0 200px', } + const Miriam = (props: any) => (
) + const Paulina = (props: any) => (
) -const TopIllustration: React.FunctionComponent = ({ children }) => { +function TopIllustration ({ children }) { return ( <> @@ -51,7 +52,7 @@ export default { Already have an account? login here ), - Miriam: () => { + Miriam: () => { // TODO: refactor using a map. const { userType } = useParams() as RouteParams return ( @@ -69,10 +70,18 @@ export default { the ReDI Career Department. I take the time to meet each mentor before they join our program." - + Miriam Abu Hamdan - + Manager Mentorship Program
Career Department ReDI School
@@ -93,16 +102,22 @@ export default { integration coordinator at the ReDI Career Department. I take the time to go through your application before you join our program."
- + Paulina MuΓ±oz - + Manager Coaching and Job Integration
Career Department ReDI School
- - paulina@redi-school.org - + paulina@redi-school.org
)} @@ -110,7 +125,7 @@ export default { ) }, - Christa: () => { + Christa: () => { // TODO: this, in addition to the previous one, can use a factory. const { userType } = useParams() as RouteParams return ( @@ -124,9 +139,9 @@ export default { > β€œHi, I am Christa, the mentorship coordinator of ReDI Connect. - {userType === 'public-sign-up-mentor-pending-review' && ( + {userType === 'public-sign-up-mentor-pending-review' && ( <> - {' '} + {' '} {/* TODO: what is this? */} I take the time to meet each mentor before they join our program." )} @@ -138,10 +153,18 @@ export default { )}
- + Christa Baron - + Head of Community Development Munich
ReDI School Munich diff --git a/apps/redi-connect/src/components/organisms/ApplicationCard.tsx b/apps/redi-connect/src/components/organisms/ApplicationCard.tsx index fb5acaf57..299f25288 100644 --- a/apps/redi-connect/src/components/organisms/ApplicationCard.tsx +++ b/apps/redi-connect/src/components/organisms/ApplicationCard.tsx @@ -2,7 +2,7 @@ import { Icon } from '@talent-connect/shared-atomic-design-components' import { RedMatch, RedProfile } from '@talent-connect/shared-types' import classnames from 'classnames' import moment from 'moment' -import React, { useState } from 'react' +import { useState } from 'react' import { Columns, Content, Heading } from 'react-bulma-components' import { connect } from 'react-redux' import { useHistory } from 'react-router-dom' @@ -20,7 +20,7 @@ interface Props { currentUser?: RedProfile } -const STATUS_LABELS: any = { +const STATUS_LABELS = { applied: 'Pending', accepted: 'Accepted', completed: 'Accepted', @@ -29,17 +29,16 @@ const STATUS_LABELS: any = { 'invalidated-as-other-mentor-accepted': 'Cancelled', } -const ApplicationCard = ({ +function ApplicationCard ({ application, hasReachedMenteeLimit, currentUser, -}: Props) => { +}: Props) { const history = useHistory() const profile = getRedProfileFromLocalStorage() const [showDetails, setShowDetails] = useState(false) const applicationDate = new Date(application.createdAt || '') - const applicationUser = - profile.userType === 'mentee' ? application.mentor : application.mentee + const applicationUser = profile.userType === 'mentee' ? application.mentor : application.mentee const currentUserIsMentor = currentUser?.userType === 'mentor' return ( @@ -60,9 +59,7 @@ const ApplicationCard = ({ > {applicationUser && ( <> -

- {applicationUser.firstName} {applicationUser.lastName} -

+

{applicationUser.firstName} {applicationUser.lastName}

{REDI_LOCATION_NAMES[applicationUser.rediLocation]}

)} @@ -75,11 +72,7 @@ const ApplicationCard = ({ - history.push( - `/app/applications/profile/${ - applicationUser && applicationUser.id - }` - ) + history.push(`/app/applications/profile/${applicationUser?.id}`) } > Visit Profile @@ -144,7 +137,7 @@ const ApplicationCard = ({ {application.expectationText} )} - {currentUserIsMentor && application.status === 'applied' ? ( + {currentUserIsMentor && application.status === 'applied' && ( <> - ) : null} + )}
) } -const mapStateToProps = (state: RootState) => ({ - currentUser: state.user.profile, - hasReachedMenteeLimit: getHasReachedMenteeLimit(state.user), +const mapStateToProps = ({ user }: RootState) => ({ + currentUser: user.profile, + hasReachedMenteeLimit: getHasReachedMenteeLimit(user), }) export default connect(mapStateToProps)(ApplicationCard) diff --git a/apps/redi-connect/src/components/organisms/ApplyForMentor.form.ts b/apps/redi-connect/src/components/organisms/ApplyForMentor.form.ts new file mode 100644 index 000000000..b19ef8a65 --- /dev/null +++ b/apps/redi-connect/src/components/organisms/ApplyForMentor.form.ts @@ -0,0 +1,49 @@ +import { Dispatch, SetStateAction } from 'react'; +import { FormSubmitResult, RedProfile } from '@talent-connect/shared-types'; +import { createComponentForm } from '@talent-connect/shared-utils'; + +import { requestMentorship } from '../../services/api/api' + +interface ComponentFormProps { + setSubmitResult: Dispatch> + setShow: Dispatch> + id: RedProfile['id']; + profilesFetchOneStart: (is: string) => void; +} + +export const componentForm = createComponentForm() + .validation((yup) => ({ + applicationText: yup.string() + .required('Write at least 250 characters to introduce yourself to your mentee.') + .min(250, 'Write at least 250 characters to introduce yourself to your mentee.') + .max(600, 'The introduction text can be up to 600 characters long.'), + expectationText: yup.string() + .required('Write at least 250 characters about your expectations.') + .min(250, 'Write at least 250 characters about your expectations.') + .max(600, 'The expectations text can be up to 600 characters long.'), + dataSharingAccepted: yup.boolean() + .required() + .oneOf([true], 'Sharing profile data with your mentor is required'), + })) + .initialValues(() => ({ + applicationText: '', + expectationText: '', + dataSharingAccepted: false, + })) + .formikConfig({ + enableReinitialize: true, + }) + .onSubmit(async ( + { applicationText, expectationText }, + actions, + { setSubmitResult, id, profilesFetchOneStart, setShow } + ) => { + setSubmitResult('submitting') + try { + await requestMentorship(applicationText, expectationText, id) + setShow(false) + profilesFetchOneStart(id) + } catch (error) { + setSubmitResult('error') + } + }) \ No newline at end of file diff --git a/apps/redi-connect/src/components/organisms/ApplyForMentor.tsx b/apps/redi-connect/src/components/organisms/ApplyForMentor.tsx index a39d89cd8..2f989cff4 100644 --- a/apps/redi-connect/src/components/organisms/ApplyForMentor.tsx +++ b/apps/redi-connect/src/components/organisms/ApplyForMentor.tsx @@ -1,6 +1,8 @@ +import { useState } from 'react' +import { connect } from 'react-redux' import { Caption, - FormTextArea, + TextArea, Checkbox, Button, } from '@talent-connect/shared-atomic-design-components' @@ -8,79 +10,27 @@ import { Modal } from '@talent-connect/shared-atomic-design-components' import { Content, Form } from 'react-bulma-components' import { FormSubmitResult, RedProfile } from '@talent-connect/shared-types' -import { FormikHelpers as FormikActions, useFormik } from 'formik' -import React, { useState } from 'react' -import * as Yup from 'yup' -import { requestMentorship } from '../../services/api/api' - import { RootState } from '../../redux/types' -import { connect } from 'react-redux' import { profilesFetchOneStart } from '../../redux/profiles/actions' - -interface ConnectionRequestFormValues { - applicationText: string - expectationText: string - dataSharingAccepted: boolean -} - -const initialValues = { - applicationText: '', - expectationText: '', - dataSharingAccepted: false, -} - -const validationSchema = Yup.object({ - applicationText: Yup.string() - .required( - 'Write at least 250 characters to introduce yourself to your mentee.' - ) - .min( - 250, - 'Write at least 250 characters to introduce yourself to your mentee.' - ) - .max(600, 'The introduction text can be up to 600 characters long.'), - expectationText: Yup.string() - .required('Write at least 250 characters about your expectations.') - .min(250, 'Write at least 250 characters about your expectations.') - .max(600, 'The expectations text can be up to 600 characters long.'), - dataSharingAccepted: Yup.boolean() - .required() - .oneOf([true], 'Sharing profile data with your mentor is required'), -}) +import { componentForm } from './ApplyForMentor.form'; interface Props { mentor: RedProfile - profilesFetchOneStart: Function + profilesFetchOneStart: (is: string) => void } -const ApplyForMentor = ({ mentor, profilesFetchOneStart }: Props) => { - const [submitResult, setSubmitResult] = useState( - 'notSubmitted' - ) +function ApplyForMentor ({ + mentor: { id, lastName, firstName }, + profilesFetchOneStart +}: Props) { + const [submitResult, setSubmitResult] = useState('notSubmitted') const [show, setShow] = useState(false) - const submitForm = async ( - values: ConnectionRequestFormValues, - actions: FormikActions - ) => { - setSubmitResult('submitting') - try { - await requestMentorship( - values.applicationText, - values.expectationText, - mentor.id - ) - setShow(false) - profilesFetchOneStart(mentor.id) - } catch (error) { - setSubmitResult('error') - } - } - - const formik = useFormik({ - enableReinitialize: true, - initialValues: initialValues, - validationSchema, - onSubmit: submitForm, + + const formik = componentForm({ + id, + setSubmitResult, + profilesFetchOneStart, + setShow, }) const handleCancel = () => { @@ -94,28 +44,27 @@ const ApplyForMentor = ({ mentor, profilesFetchOneStart }: Props) => {
- {submitResult === 'success' && ( - <>Your application was successfully submitted. - )} - {submitResult !== 'success' && ( + {submitResult === 'success' + ? (<>Your application was successfully submitted.) + : ( <> Motivation

- Write an application to the {mentor.firstName}{' '} - {mentor.lastName} in which you describe why you think that + Write an application to the {firstName}{' '} + {lastName} in which you describe why you think that the two of you are a great fit.

- { mentorship with this mentor.

- { ) } -const mapStateToProps = (state: RootState) => ({ - mentor: state.profiles.oneProfile as RedProfile, -}) +const mapStateToProps = ({ profiles }: RootState) => ({ mentor: profiles.oneProfile }) -const mapDispatchToProps = (dispatch: any) => ({ +const mapDispatchToProps = (dispatch: Function) => ({ profilesFetchOneStart: (profileId: string) => dispatch(profilesFetchOneStart(profileId)), }) diff --git a/apps/redi-connect/src/components/organisms/Avatar.form.ts b/apps/redi-connect/src/components/organisms/Avatar.form.ts new file mode 100644 index 000000000..5984557cd --- /dev/null +++ b/apps/redi-connect/src/components/organisms/Avatar.form.ts @@ -0,0 +1,12 @@ +import { createComponentForm } from '@talent-connect/shared-utils'; + +export const componentForm = createComponentForm() + .validation((yup) => ({ + + })) + .initialValues(() => ({ + + })) + .onSubmit(() => { + + }) \ No newline at end of file diff --git a/apps/redi-connect/src/components/organisms/Avatar.scss b/apps/redi-connect/src/components/organisms/Avatar.scss index 06ffe2ca5..b74362cb9 100644 --- a/apps/redi-connect/src/components/organisms/Avatar.scss +++ b/apps/redi-connect/src/components/organisms/Avatar.scss @@ -1,3 +1,4 @@ +@use '_colors' as colors; @import '~bulma/sass/utilities/_all'; @import '_variables.scss'; @@ -23,7 +24,7 @@ } &--editable { - border: 4px solid $redi-orange-dark; + border: 4px solid colors.$redi-orange-dark; box-shadow: 2px 0 15px 0 rgba(255, 92, 31, 0.34); padding-top: calc(100% - 8px); @@ -62,7 +63,7 @@ &--placeholder:before { border: 1px solid #a0a0a0; - background-color: $grey-extra-light; + background-color: colors.$grey-extra-light; } &__image { @@ -86,8 +87,8 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - border: 1px solid $grey-light; - color: $grey-light; + border: 1px solid colors.$grey-light; + color: colors.$grey-light; border-radius: $radius; } diff --git a/apps/redi-connect/src/components/organisms/Avatar.tsx b/apps/redi-connect/src/components/organisms/Avatar.tsx index 517e26761..747bc3fdf 100644 --- a/apps/redi-connect/src/components/organisms/Avatar.tsx +++ b/apps/redi-connect/src/components/organisms/Avatar.tsx @@ -1,8 +1,7 @@ -import React from 'react' import { ReactComponent as UploadImage } from '../../assets/images/uploadImage.svg' import ReactS3Uploader from 'react-s3-uploader' import { Element } from 'react-bulma-components' -import { FormikValues, useFormik } from 'formik' +import { useFormik } from 'formik' import * as Yup from 'yup' import { AWS_PROFILE_AVATARS_BUCKET_BASE_URL, @@ -11,32 +10,27 @@ import { import classnames from 'classnames' import placeholderImage from '../../assets/images/img-placeholder.png' -import { RootState } from '../../redux/types' import { connect } from 'react-redux' import './Avatar.scss' import { profileSaveStart } from '../../redux/user/actions' import { RedProfile } from '@talent-connect/shared-types' +import { mapStateToProps } from '../../helpers'; + +// TODO: this looks like a repeated component. Move to shared components? interface AvatarProps { profile: RedProfile } -interface AvatarEditable { - profile: RedProfile - profileSaveStart: Function -} - -interface AvatarFormValues { - profileAvatarImageS3Key: string -} const validationSchema = Yup.object({ profileAvatarImageS3Key: Yup.string().max(255), }) -const Avatar = ({ profile }: AvatarProps) => { - const { profileAvatarImageS3Key } = profile +function Avatar ({ + profile: { profileAvatarImageS3Key, firstName, lastName } +}: AvatarProps) { const imgSrc = profileAvatarImageS3Key ? AWS_PROFILE_AVATARS_BUCKET_BASE_URL + profileAvatarImageS3Key : placeholderImage @@ -49,30 +43,36 @@ const Avatar = ({ profile }: AvatarProps) => { > {`${profile.firstName}
) } -const AvatarEditable = ({ profile, profileSaveStart }: AvatarEditable) => { - const { profileAvatarImageS3Key } = profile - const imgURL = AWS_PROFILE_AVATARS_BUCKET_BASE_URL + profileAvatarImageS3Key +interface AvatarFormValues { + profileAvatarImageS3Key: string +} - const submitForm = async (values: FormikValues) => { - const profileMe = values as Partial - profileSaveStart({ ...profileMe, id: profile.id }) - } +interface AvatarEditable { + profile: RedProfile + profileSaveStart: (arg: AvatarFormValues & { id: string; }) => void +} - const initialValues: AvatarFormValues = { - profileAvatarImageS3Key: profileAvatarImageS3Key, - } +function AvatarEditable ({ + profile: { profileAvatarImageS3Key, id, firstName, lastName }, + profileSaveStart +}: AvatarEditable) { + const imgURL = AWS_PROFILE_AVATARS_BUCKET_BASE_URL + profileAvatarImageS3Key - const formik = useFormik({ - initialValues: initialValues, + const formik = useFormik({ + initialValues: { + profileAvatarImageS3Key + }, validationSchema, - onSubmit: submitForm, + onSubmit: (profileMe) => { + profileSaveStart({ ...profileMe, id }) + }, }) const onUploadSuccess = (result: any) => { @@ -90,7 +90,7 @@ const AvatarEditable = ({ profile, profileSaveStart }: AvatarEditable) => { <> {`${profile.firstName} { signingUrl={S3_UPLOAD_SIGN_URL} accept="image/*" uploadRequestHeaders={{ 'x-amz-acl': 'public-read' }} - onSignedUrl={(c: any) => console.log(c)} - onError={(c: any) => console.log(c)} + onSignedUrl={(c) => console.log(c)} + onError={(c) => console.log(c)} onFinish={onUploadSuccess} contentDisposition="auto" /> @@ -135,11 +135,7 @@ const AvatarEditable = ({ profile, profileSaveStart }: AvatarEditable) => { ) } -const mapStateToProps = (state: RootState) => ({ - profile: state.user.profile as RedProfile, -}) - -const mapDispatchToProps = (dispatch: any) => ({ +const mapDispatchToProps = (dispatch: Function) => ({ profileSaveStart: (profile: Partial) => dispatch(profileSaveStart(profile)), }) diff --git a/apps/redi-connect/src/components/organisms/Carousel.scss b/apps/redi-connect/src/components/organisms/Carousel.scss index 27cf7e114..a0a8ea373 100644 --- a/apps/redi-connect/src/components/organisms/Carousel.scss +++ b/apps/redi-connect/src/components/organisms/Carousel.scss @@ -1,3 +1,4 @@ +@use '_colors' as colors; @import '~bulma/sass/utilities/_all'; @import '_variables.scss'; @@ -10,7 +11,7 @@ .slick-dots li { width: 0.75rem; height: 0.75rem; - background: $grey-light; + background: colors.$grey-light; border-radius: 50%; & button:before { @@ -18,7 +19,7 @@ } &.slick-active { - background: $red; + background: colors.$red; } } @@ -38,11 +39,11 @@ } &--border-orange { - border-image: linear-gradient(to right, $redi-orange 30%, white 30%) 10%; + border-image: linear-gradient(to right, colors.$redi-orange 30%, white 30%) 10%; } &--border-blue { - border-image: linear-gradient(to right, $redi-blue 30%, white 30%) 10%; + border-image: linear-gradient(to right, colors.$redi-blue 30%, white 30%) 10%; } } } diff --git a/apps/redi-connect/src/components/organisms/Carousel.tsx b/apps/redi-connect/src/components/organisms/Carousel.tsx index 59d3aa740..a1668a603 100644 --- a/apps/redi-connect/src/components/organisms/Carousel.tsx +++ b/apps/redi-connect/src/components/organisms/Carousel.tsx @@ -1,4 +1,5 @@ -import React from 'react' +import { useTranslation } from 'react-i18next' +import Slider from 'react-slick' import { Section, Container, @@ -7,9 +8,7 @@ import { Content, Image, } from 'react-bulma-components' -import { useTranslation } from 'react-i18next' import classnames from 'classnames' -import Slider from 'react-slick' import khaled from '../../assets/images/profile-khaled.jpg' import halil from '../../assets/images/profile-halil.jpg' import dragos from '../../assets/images/profile-dragos.jpg' @@ -22,7 +21,7 @@ interface Props { border: 'blue' | 'orange' } -const Carousel = ({ headline, title, border }: Props) => { +function Carousel ({ headline, title, border }: Props) { const { t } = useTranslation() const quotes = [ @@ -60,14 +59,14 @@ const Carousel = ({ headline, title, border }: Props) => { - {quotes.map((quote: any) => { + {quotes.map(({ img, title, text }) => { return ( -
+
{quote.title} { className="decoration decoration--bottomLeft oneandhalf-bs" responsive={{ mobile: { hide: { value: true } } }} > - {quote.title} + {title} - {quote.text} + {text} { className="oneandhalf-bs" responsive={{ desktop: { hide: { value: true } } }} > - {quote.title} + {title} diff --git a/apps/redi-connect/src/components/organisms/Checklist.tsx b/apps/redi-connect/src/components/organisms/Checklist.tsx index 78604fb6c..4fd725259 100644 --- a/apps/redi-connect/src/components/organisms/Checklist.tsx +++ b/apps/redi-connect/src/components/organisms/Checklist.tsx @@ -1,22 +1,20 @@ -import React from 'react' -import { Section, Container, Element } from 'react-bulma-components' import { useTranslation } from 'react-i18next' + +import { Section, Container, Element } from 'react-bulma-components' import { DecoratedHeadline } from '@talent-connect/shared-atomic-design-components' import './Checklist.scss' interface Props { type: 'mentor' | 'mentee' } -const Checklist = ({ type }: Props) => { +function Checklist ({ type }: Props) { const { t } = useTranslation() - const checklist: Array<{ + const checklist: { content: string headline: string image: any - }> = t(`loggedOutArea.homePage.checklist.${type}.items`, { - returnObjects: true, - }) + }[] = t(`loggedOutArea.homePage.checklist.${type}.items`, { returnObjects: true }) return (
diff --git a/apps/redi-connect/src/components/organisms/CompleteMentorship.form.ts b/apps/redi-connect/src/components/organisms/CompleteMentorship.form.ts new file mode 100644 index 000000000..b43c5a953 --- /dev/null +++ b/apps/redi-connect/src/components/organisms/CompleteMentorship.form.ts @@ -0,0 +1,28 @@ +import { Dispatch, SetStateAction } from 'react'; +import { createComponentForm } from '@talent-connect/shared-utils'; + +interface ComponentFormProps { + id: string; + setModalActive: Dispatch>; + matchesMarkAsComplete: (redMatchId: string, mentorMessageOnComplete: string) => void +} + +export const componentForm = createComponentForm() + .validation((yup) => ({ + mentorMessageOnComplete: yup.string(), + })) + .initialValues(() => ({ + mentorMessageOnComplete: '', + })) + .onSubmit(( + { mentorMessageOnComplete }, + actions, + { matchesMarkAsComplete, id, setModalActive } + ) => { + try { + matchesMarkAsComplete(id, mentorMessageOnComplete) + setModalActive(false) + } catch (error) { + console.log('error ', error) + } + }) \ No newline at end of file diff --git a/apps/redi-connect/src/components/organisms/CompleteMentorship.tsx b/apps/redi-connect/src/components/organisms/CompleteMentorship.tsx index f2428bd4d..06a086573 100644 --- a/apps/redi-connect/src/components/organisms/CompleteMentorship.tsx +++ b/apps/redi-connect/src/components/organisms/CompleteMentorship.tsx @@ -1,49 +1,30 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { connect } from 'react-redux' -import { useFormik } from 'formik' import { Content } from 'react-bulma-components' import { - FormTextArea, + TextArea, Button, } from '@talent-connect/shared-atomic-design-components' import { Modal } from '@talent-connect/shared-atomic-design-components' import { matchesMarkAsComplete } from '../../redux/matches/actions' import { RedMatch } from '@talent-connect/shared-types' +import { componentForm } from './CompleteMentorship.form'; interface CompleteMentorshipProps { match: RedMatch - matchesMarkAsComplete: ( - redMatchId: string, - mentorMessageOnComplete: string - ) => void + matchesMarkAsComplete: (redMatchId: string, mentorMessageOnComplete: string) => void } -interface CompleteMentorshipFormValues { - mentorMessageOnComplete: string -} - -const initialValues = { - mentorMessageOnComplete: '', -} - -const CompleteMentorship = ({ - match, +function CompleteMentorship ({ + match: { id }, matchesMarkAsComplete, -}: CompleteMentorshipProps) => { +}: CompleteMentorshipProps) { const [isModalActive, setModalActive] = useState(false) - const submitForm = async (values: CompleteMentorshipFormValues) => { - try { - matchesMarkAsComplete(match.id, values.mentorMessageOnComplete) - setModalActive(false) - } catch (error) { - console.log('error ', error) - } - } - - const formik = useFormik({ - initialValues, - onSubmit: submitForm, + const formik = componentForm({ + id, + matchesMarkAsComplete, + setModalActive, }) const handleCancel = () => { @@ -71,19 +52,17 @@ const CompleteMentorship = ({ the platform.

- -
- ) : null} + )} {/* */} diff --git a/apps/redi-connect/src/pages/front/landing/Mentor.tsx b/apps/redi-connect/src/pages/front/landing/Mentor.tsx index cfc016084..19933b528 100644 --- a/apps/redi-connect/src/pages/front/landing/Mentor.tsx +++ b/apps/redi-connect/src/pages/front/landing/Mentor.tsx @@ -1,15 +1,15 @@ -import { Button } from '@talent-connect/shared-atomic-design-components' -import React from 'react' import { useTranslation } from 'react-i18next' +import { useHistory } from 'react-router-dom' +import { Section, Container } from 'react-bulma-components' + +import { Button } from '@talent-connect/shared-atomic-design-components' import Checklist from '../../../components/organisms/Checklist' import PreFooter from '../../../components/organisms/PreFooter' import RediHeroLanding from '../../../components/organisms/RediHeroLanding' import Landing from '../../../components/templates/Landing' import { isLoggedIn } from '../../../services/auth/auth' -import { Section, Container } from 'react-bulma-components' -import { useHistory } from 'react-router-dom' -const Mentor = () => { +function Mentor() { const { t } = useTranslation() const title = t('loggedOutArea.homePage.carousel.titleMentorOrMentee') const headline = t('loggedOutArea.homePage.carousel.headlineMentorOrMentee') @@ -19,7 +19,7 @@ const Mentor = () => { - {!isLoggedIn() ? ( + {!isLoggedIn() && (
- ) : null} + )} {/* */}
diff --git a/apps/redi-connect/src/pages/front/login/Login.form.ts b/apps/redi-connect/src/pages/front/login/Login.form.ts new file mode 100644 index 000000000..5984557cd --- /dev/null +++ b/apps/redi-connect/src/pages/front/login/Login.form.ts @@ -0,0 +1,12 @@ +import { createComponentForm } from '@talent-connect/shared-utils'; + +export const componentForm = createComponentForm() + .validation((yup) => ({ + + })) + .initialValues(() => ({ + + })) + .onSubmit(() => { + + }) \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/login/Login.tsx b/apps/redi-connect/src/pages/front/login/Login.tsx index bd4c4d54e..84a05fa1f 100644 --- a/apps/redi-connect/src/pages/front/login/Login.tsx +++ b/apps/redi-connect/src/pages/front/login/Login.tsx @@ -1,9 +1,9 @@ -import React, { useState, useCallback } from 'react' +import { useState, useCallback } from 'react' import AccountOperation from '../../../components/templates/AccountOperation' import Teaser from '../../../components/molecules/Teaser' import * as Yup from 'yup' import { Link } from 'react-router-dom' -import { FormikHelpers as FormikActions, FormikValues, useFormik } from 'formik' +import { useFormik } from 'formik' import { history } from '../../../services/history/history' import { login, fetchSaveRedProfile } from '../../../services/api/api' import { @@ -18,7 +18,7 @@ import { buildFrontendUrl } from '../../../utils/build-frontend-url' import { REDI_LOCATION_NAMES } from '@talent-connect/shared-config' import { Button, - FormInput, + TextInput, Heading, } from '@talent-connect/shared-atomic-design-components' @@ -27,52 +27,48 @@ interface LoginFormValues { password: string } -const initialValues: LoginFormValues = { - username: '', - password: '', -} - const validationSchema = Yup.object({ - username: Yup.string().email().required().label('Email').max(255), - password: Yup.string().required().label('Password').max(255), + username: Yup.string() + .email() + .required() + .label('Email') + .max(255), + password: Yup.string() + .required() + .label('Password') + .max(255), }) -export default function Login() { +function Login() { const [loginError, setLoginError] = useState('') const [isWrongRediLocationError, setIsWrongRediLocationError] = useState(false) - const submitForm = useCallback((values, actions) => { - ;(async (values: FormikValues, actions: FormikActions) => { - const formValues = values as LoginFormValues - try { - const accessToken = await login( - formValues.username, - formValues.password - ) - saveAccessTokenToLocalStorage(accessToken) - const redProfile = await fetchSaveRedProfile(accessToken) - if ( - redProfile.rediLocation !== - (process.env.NX_REDI_CONNECT_REDI_LOCATION as RediLocation) - ) { - setIsWrongRediLocationError(true) - purgeAllSessionData() - return - } - actions.setSubmitting(false) - history.push('/app/me') - } catch (err) { - actions.setSubmitting(false) - setLoginError('You entered an incorrect email, password, or both.') - } - })(values, actions) - }, []) - - const formik = useFormik({ - initialValues, + const formik = useFormik({ + initialValues: { + username: '', + password: '', + }, validationSchema, - onSubmit: submitForm, + onSubmit: useCallback(({ username, password }, actions) => { + (async () => { + try { + const accessToken = await login(username, password) + saveAccessTokenToLocalStorage(accessToken) + const { rediLocation } = await fetchSaveRedProfile(accessToken) + if (rediLocation !== (process.env.NX_REDI_CONNECT_REDI_LOCATION as RediLocation)) { + setIsWrongRediLocationError(true) + purgeAllSessionData() + return + } + actions.setSubmitting(false) + history.push('/app/me') + } catch (err) { + actions.setSubmitting(false) + setLoginError('You entered an incorrect email, password, or both.') + } + })() + }, []), }) return ( @@ -100,9 +96,7 @@ export default function Login() { You've tried to log into ReDI Connect{' '} { - REDI_LOCATION_NAMES[ - process.env.NX_REDI_CONNECT_REDI_LOCATION as RediLocation - ] + REDI_LOCATION_NAMES[process.env.NX_REDI_CONNECT_REDI_LOCATION as RediLocation] } , but your account is linked to ReDI Connect{' '} @@ -127,14 +121,14 @@ export default function Login() { )}
e.preventDefault()}> - - ) } + +export default Login \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/reset-password/RequestResetPasswordEmail.form.ts b/apps/redi-connect/src/pages/front/reset-password/RequestResetPasswordEmail.form.ts new file mode 100644 index 000000000..5984557cd --- /dev/null +++ b/apps/redi-connect/src/pages/front/reset-password/RequestResetPasswordEmail.form.ts @@ -0,0 +1,12 @@ +import { createComponentForm } from '@talent-connect/shared-utils'; + +export const componentForm = createComponentForm() + .validation((yup) => ({ + + })) + .initialValues(() => ({ + + })) + .onSubmit(() => { + + }) \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/reset-password/RequestResetPasswordEmail.tsx b/apps/redi-connect/src/pages/front/reset-password/RequestResetPasswordEmail.tsx index 334b1ac60..ee1f045af 100644 --- a/apps/redi-connect/src/pages/front/reset-password/RequestResetPasswordEmail.tsx +++ b/apps/redi-connect/src/pages/front/reset-password/RequestResetPasswordEmail.tsx @@ -1,12 +1,12 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { Columns, Content, Element, Form } from 'react-bulma-components' import { Heading, Button, - FormInput, + TextInput, } from '@talent-connect/shared-atomic-design-components' import { AccountOperation } from '../../../components/templates' -import { FormikValues, useFormik } from 'formik' +import { useFormik } from 'formik' import * as yup from 'yup' import { Link } from 'react-router-dom' @@ -17,39 +17,30 @@ interface FormValues { email: string } -const initialValues: FormValues = { - email: '', -} - const validationSchema = yup.object().shape({ - email: yup - .string() + email: yup.string() .email('That doesn’t look quite right... please provide a valid email.') .required('Please provide an email address.'), }) -export const RequestResetPasswordEmail: React.FC = () => { +export function RequestResetPasswordEmail() { const [resetPasswordSuccess, setResetPasswordSuccess] = useState('') const [resetPasswordError, setResetPasswordError] = useState('') - const onSubmit = async (values: FormikValues) => { - try { - // Cast to string is safe as this only called if validated - await requestResetPasswordEmail(values.email as string) - setResetPasswordSuccess( - 'If you have an account,we have sent you the password reset link to your email address.' - ) - } catch (err) { - setResetPasswordError( - 'Oh no, something went wrong :( Did you type your email address correctly?' - ) - } - } - - const formik = useFormik({ - initialValues: initialValues, + const formik = useFormik({ + initialValues: { + email: '', + }, validationSchema, - onSubmit: onSubmit, + onSubmit: async ({ email }) => { + try { + // Cast to string is safe as this only called if validated + await requestResetPasswordEmail(email) + setResetPasswordSuccess('If you have an account,we have sent you the password reset link to your email address.') + } catch (err) { + setResetPasswordError( 'Oh no, something went wrong :( Did you type your email address correctly?') + } + }, }) const heading = resetPasswordSuccess @@ -77,7 +68,7 @@ export const RequestResetPasswordEmail: React.FC = () => { {!resetPasswordSuccess && ( e.preventDefault()}> - > +} + +export const componentForm = createComponentForm() + .validation((yup) => ({ + password: yup.string() + .min(8, 'Password must contain at least 8 characters') + .required('Enter your password') + .label('Password'), + passwordConfirm: yup.string() + .required('Confirm your password') + .oneOf([yup.ref('password')], 'Password does not match'), + })) + .initialValues(() => ({ + password: '', + passwordConfirm: '', + })) + .onSubmit(async ({ password }, { setSubmitting }, { setFormError }) => { + try { + await setPassword(password) + showNotification("Your new password is set and you're logged in :)", { + variant: 'success', + autoHideDuration: 8000, + }) + history.push('/app/me') + } catch (err) { + setFormError('Invalid username or password') + } + setSubmitting(false) + }) \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/reset-password/SetNewPassword.tsx b/apps/redi-connect/src/pages/front/reset-password/SetNewPassword.tsx index dcf4e3e29..489a52240 100644 --- a/apps/redi-connect/src/pages/front/reset-password/SetNewPassword.tsx +++ b/apps/redi-connect/src/pages/front/reset-password/SetNewPassword.tsx @@ -1,54 +1,31 @@ -import React, { useState, useEffect } from 'react' -import AccountOperation from '../../../components/templates/AccountOperation' -import Teaser from '../../../components/molecules/Teaser' -import { Columns, Content, Form } from 'react-bulma-components' +import { useState, useEffect } from 'react' +import { RouteComponentProps } from 'react-router' import { Link } from 'react-router-dom' +import { Columns, Content, Form } from 'react-bulma-components' -import * as Yup from 'yup' +import AccountOperation from '../../../components/templates/AccountOperation' +import Teaser from '../../../components/molecules/Teaser' -import { FormikHelpers as FormikActions, FormikValues, useFormik } from 'formik' -import { history } from '../../../services/history/history' -import { setPassword, fetchSaveRedProfile } from '../../../services/api/api' +import { fetchSaveRedProfile } from '../../../services/api/api' import { saveAccessTokenToLocalStorage } from '../../../services/auth/auth' -import { RouteComponentProps } from 'react-router' -import { showNotification } from '../../../components/AppNotification' import { Button, - FormInput, + TextInput, Heading, } from '@talent-connect/shared-atomic-design-components' - -interface SetNewPasswordValues { - password: string - passwordConfirm: string -} - -const initialValues: SetNewPasswordValues = { - password: '', - passwordConfirm: '', -} - -const validationSchema = Yup.object({ - password: Yup.string() - .min(8, 'Password must contain at least 8 characters') - .required('Enter your password') - .label('Password'), - passwordConfirm: Yup.string() - .required('Confirm your password') - .oneOf([Yup.ref('password')], 'Password does not match'), -}) +import { componentForm } from './SetNewPassword.form'; interface RouteParams { accessToken: string } -export const SetNewPassword = (props: RouteComponentProps) => { +export function SetNewPassword ({ match }: RouteComponentProps) { const [formError, setFormError] = useState('') const [errorMsg, setErrorMsg] = useState('') useEffect(() => { const load = async () => { - const accessTokenStr = decodeURIComponent(props.match.params.accessToken) + const accessTokenStr = decodeURIComponent(match.params.accessToken) let accessToken try { accessToken = JSON.parse(accessTokenStr) @@ -56,45 +33,21 @@ export const SetNewPassword = (props: RouteComponentProps) => { console.log('savetoken') } catch (err) { console.log('savetoken errp') - return setErrorMsg( - 'Sorry, there seems to have been an error. Please try to reset your password again, or contact career@redi-school.org for assistance.' - ) + return setErrorMsg('Sorry, there seems to have been an error. Please try to reset your password again, or contact career@redi-school.org for assistance.') } try { await fetchSaveRedProfile(accessToken) console.log('saveprofile') } catch (err) { console.log('saveprofile error') - - return setErrorMsg( - 'Sorry, the link you used seems to have expired. Please contact career@redi-school.org to receive a new one.' - ) + return setErrorMsg('Sorry, the link you used seems to have expired. Please contact career@redi-school.org to receive a new one.') } } load() - }, [props.match.params.accessToken]) - - const submitForm = async ( - values: FormikValues, - actions: FormikActions - ) => { - try { - await setPassword(values.password) - showNotification("Your new password is set and you're logged in :)", { - variant: 'success', - autoHideDuration: 8000, - }) - history.push('/app/me') - } catch (err) { - setFormError('Invalid username or password') - } - actions.setSubmitting(false) - } + }, [match.params.accessToken]) - const formik = useFormik({ - initialValues: initialValues, - validationSchema, - onSubmit: submitForm, + const formik = componentForm({ + setFormError }) return ( @@ -115,17 +68,17 @@ export const SetNewPassword = (props: RouteComponentProps) => { {formError && { formError }} e.preventDefault()}> - - diff --git a/apps/redi-connect/src/pages/front/signup/SignUp.form.ts b/apps/redi-connect/src/pages/front/signup/SignUp.form.ts new file mode 100644 index 000000000..69f5637c8 --- /dev/null +++ b/apps/redi-connect/src/pages/front/signup/SignUp.form.ts @@ -0,0 +1,94 @@ +import { Dispatch, SetStateAction } from 'react'; +import { omit } from 'lodash'; + +import { createComponentForm } from '@talent-connect/shared-utils'; +import { courses } from '../../../config/config' +import { signUp } from '../../../services/api/api' +import { history } from '../../../services/history/history' +import { Extends, RedProfile } from '@talent-connect/shared-types'; + + +export type SignUpUserType = Extends< // TODO necessary? + RedProfile['userType'], + | 'public-sign-up-mentee-pending-review' + | 'public-sign-up-mentor-pending-review' +> + +interface ComponentFormProps { + userType: SignUpUserType; + setSubmitError: Dispatch>; +} + +const coursesIds = courses.map((level) => level.id) + +export const componentForm = createComponentForm() + .validation((yup) => ({ + firstName: yup.string() + .required('Your first name is invalid').max(255), + lastName: yup.string() + .required('Your last name is invalid').max(255), + contactEmail: yup.string() + .email('Your email is invalid') + .required('You need to give an email address') + .label('Email') + .max(255), + password: yup.string() + .min(8, 'The password has to consist of at least eight characters') + .required('You need to set a password') + .label('Password'), + passwordConfirm: yup.string() + .required('Confirm your password') + .oneOf([yup.ref('password')], 'Passwords does not match'), + agreesWithCodeOfConduct: yup.boolean() + .required() + .oneOf([true]), + gaveGdprConsent: yup.boolean() + .required() + .oneOf([true]), + mentee_currentlyEnrolledInCourse: yup.string() + .when('userType', { + is: 'public-sign-up-mentee-pending-review', + then: yup.string() + .required() + .oneOf(coursesIds) + .label('Currently enrolled in course'), + }), + })) + .initialValues(({ userType }) => ({ + userType, + gaveGdprConsent: false, + contactEmail: '', + password: '', + passwordConfirm: '', + firstName: '', + lastName: '', + agreesWithCodeOfConduct: false, + mentee_currentlyEnrolledInCourse: '', + })) + .formikConfig({ + enableReinitialize: true, + }) + .onSubmit(async (profile, { setSubmitting }, { setSubmitError, userType }) => { + setSubmitError(false) + // TODO: this needs to be done in a smarter way, like iterating over the RedProfile definition or something + const cleanProfile = omit(profile, [ + 'password', + 'passwordConfirm', + 'agreesWithCodeOfConduct', + 'gaveGdprConsent', + ]) + const complementedProfile = { + ...cleanProfile, + userActivated: false, + signupSource: 'public-sign-up' as 'public-sign-up', + menteeCountCapacity: 1, + } + try { + await signUp(profile.contactEmail, profile.password, complementedProfile) + setSubmitting(false) + history.push(`/front/signup-email-verification/${userType}`) + } catch (error) { + setSubmitting(false) + setSubmitError(!!error) + } + }) \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/signup/SignUp.tsx b/apps/redi-connect/src/pages/front/signup/SignUp.tsx index fd20b60f0..e298d7e3e 100644 --- a/apps/redi-connect/src/pages/front/signup/SignUp.tsx +++ b/apps/redi-connect/src/pages/front/signup/SignUp.tsx @@ -1,16 +1,12 @@ -import React, { useState } from 'react' +import { useState } from 'react' import AccountOperation from '../../../components/templates/AccountOperation' import { useParams } from 'react-router' import { Link } from 'react-router-dom' -import * as Yup from 'yup' - -import { FormikValues, FormikHelpers as FormikActions, useFormik } from 'formik' -import omit from 'lodash/omit' import { Button, Checkbox, - FormInput, + TextInput, FormSelect, Heading, } from '@talent-connect/shared-atomic-design-components' @@ -19,52 +15,16 @@ import Teaser from '../../../components/molecules/Teaser' import { Columns, Content, Form } from 'react-bulma-components' -import { signUp } from '../../../services/api/api' -import { Extends, RedProfile } from '@talent-connect/shared-types' -import { history } from '../../../services/history/history' +import { UserRole } from '@talent-connect/shared-types' import { courses } from '../../../config/config' +import { componentForm, SignUpUserType } from './SignUp.form'; -const formCourses = courses.map((course) => ({ - value: course.id, - label: course.label, -})) - -export const validationSchema = Yup.object({ - firstName: Yup.string().required('Your first name is invalid').max(255), - lastName: Yup.string().required('Your last name is invalid').max(255), - contactEmail: Yup.string() - .email('Your email is invalid') - .required('You need to give an email address') - .label('Email') - .max(255), - password: Yup.string() - .min(8, 'The password has to consist of at least eight characters') - .required('You need to set a password') - .label('Password'), - passwordConfirm: Yup.string() - .required('Confirm your password') - .oneOf([Yup.ref('password')], 'Passwords does not match'), - agreesWithCodeOfConduct: Yup.boolean().required().oneOf([true]), - gaveGdprConsent: Yup.boolean().required().oneOf([true]), - mentee_currentlyEnrolledInCourse: Yup.string().when('userType', { - is: 'public-sign-up-mentee-pending-review', - then: Yup.string() - .required() - .oneOf(courses.map((level) => level.id)) - .label('Currently enrolled in course'), - }), -}) +const formCourses = courses.map(({ id, label }) => ({ value: id, label })) type SignUpPageType = { - type: 'mentor' | 'mentee' + type: UserRole } -type SignUpUserType = Extends< - RedProfile['userType'], - | 'public-sign-up-mentee-pending-review' - | 'public-sign-up-mentor-pending-review' -> - export interface SignUpFormValues { userType: SignUpUserType gaveGdprConsent: boolean @@ -77,60 +37,20 @@ export interface SignUpFormValues { mentee_currentlyEnrolledInCourse: string } -export default function SignUp() { - const { type } = useParams() +function SignUp() { + const { type } = useParams() as SignUpPageType // we may consider removing the backend types from frontend - const userType: SignUpUserType = - type === 'mentee' + const userType: SignUpUserType = type === 'mentee' ? 'public-sign-up-mentee-pending-review' : 'public-sign-up-mentor-pending-review' - const initialValues: SignUpFormValues = { - userType, - gaveGdprConsent: false, - contactEmail: '', - password: '', - passwordConfirm: '', - firstName: '', - lastName: '', - agreesWithCodeOfConduct: false, - mentee_currentlyEnrolledInCourse: '', - } - const [submitError, setSubmitError] = useState(false) - const submitForm = async ( - values: FormikValues, - actions: FormikActions - ) => { - setSubmitError(false) - const profile = values as Partial - // TODO: this needs to be done in a smarter way, like iterating over the RedProfile definition or something - const cleanProfile: Partial = omit(profile, [ - 'password', - 'passwordConfirm', - 'agreesWithCodeOfConduct', - 'gaveGdprConsent', - ]) - cleanProfile.userActivated = false - cleanProfile.signupSource = 'public-sign-up' - cleanProfile.menteeCountCapacity = 1 - try { - await signUp(values.contactEmail, values.password, cleanProfile) - actions.setSubmitting(false) - history.push(`/front/signup-email-verification/${cleanProfile.userType}`) - } catch (error) { - actions.setSubmitting(false) - setSubmitError(Boolean(error)) - } - } - - const formik = useFormik({ - enableReinitialize: true, - initialValues: initialValues, - validationSchema, - onSubmit: submitForm, - }) + + const formik = componentForm({ + setSubmitError, + userType + }); return ( @@ -152,33 +72,33 @@ export default function SignUp() { )} e.preventDefault()} className="form"> - - - - - ) } + +export default SignUp \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/signup/SignUpComplete.tsx b/apps/redi-connect/src/pages/front/signup/SignUpComplete.tsx index 5e2fe16c4..ed082c2ba 100644 --- a/apps/redi-connect/src/pages/front/signup/SignUpComplete.tsx +++ b/apps/redi-connect/src/pages/front/signup/SignUpComplete.tsx @@ -1,4 +1,3 @@ -import React from 'react' import AccountOperation from '../../../components/templates/AccountOperation' import { ReactComponent as WelcomeIllustration } from '../../../assets/images/welcome-user.svg' import { Columns, Form, Content } from 'react-bulma-components' @@ -13,9 +12,9 @@ type RouteParams = { userType: UserType } -export default function SignUpComplete() { +function SignUpComplete () { const history = useHistory() - const { userType } = useParams() as RouteParams + const { userType } = useParams() return ( @@ -71,3 +70,5 @@ export default function SignUpComplete() { ) } + +export default SignUpComplete \ No newline at end of file diff --git a/apps/redi-connect/src/pages/front/signup/SignUpEmailVerification.tsx b/apps/redi-connect/src/pages/front/signup/SignUpEmailVerification.tsx index 3550bc4f0..d629f08f1 100644 --- a/apps/redi-connect/src/pages/front/signup/SignUpEmailVerification.tsx +++ b/apps/redi-connect/src/pages/front/signup/SignUpEmailVerification.tsx @@ -1,33 +1,33 @@ -import React from 'react' import AccountOperation from '../../../components/templates/AccountOperation' import { Columns, Content } from 'react-bulma-components' import Teaser from '../../../components/molecules/Teaser' import { Heading } from '@talent-connect/shared-atomic-design-components' -import { envRediLocation } from '../../../utils/env-redi-location' -const SignUpEmailVerification = () => ( - - - - - +function SignUpEmailVerification () { + return ( + + + + + - - Welcome to ReDI Connect - -

Thank you for signing up!

-

- Please first verify your email address with the - email we just sent to you. -

-

Then, we are ReDI to get to know you better!

-
-
-
-
-) + + Welcome to ReDI Connect + +

Thank you for signing up!

+

+ Please first verify your email address with the + email we just sent to you. +

+

Then, we are ReDI to get to know you better!

+
+
+
+
+ ); +} export default SignUpEmailVerification diff --git a/apps/redi-connect/src/pages/front/signup/SignUpLanding.scss b/apps/redi-connect/src/pages/front/signup/SignUpLanding.scss index 1f1c89784..4422d5704 100644 --- a/apps/redi-connect/src/pages/front/signup/SignUpLanding.scss +++ b/apps/redi-connect/src/pages/front/signup/SignUpLanding.scss @@ -1,7 +1,8 @@ +@use '_colors' as colors; @import '~bulma/sass/utilities/_all'; @import '_variables.scss'; -.signup { +.sign-up { margin-bottom: 3.875rem; display: flex; align-items: center; @@ -39,9 +40,9 @@ } .border-mentor { - border-color: $redi-blue-dark; + border-color: colors.$redi-blue-dark; } .border-mentee { - border-color: $redi-orange-dark; + border-color: colors.$redi-orange-dark; } diff --git a/apps/redi-connect/src/pages/front/signup/SignUpLanding.tsx b/apps/redi-connect/src/pages/front/signup/SignUpLanding.tsx index b5ab3be00..9e767e81f 100644 --- a/apps/redi-connect/src/pages/front/signup/SignUpLanding.tsx +++ b/apps/redi-connect/src/pages/front/signup/SignUpLanding.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react' +import { useState } from 'react' import { useHistory } from 'react-router-dom' import { Content, Columns, Element } from 'react-bulma-components' import AccountOperation from '../../../components/templates/AccountOperation' @@ -8,22 +8,21 @@ import { Heading, SVGImage, } from '@talent-connect/shared-atomic-design-components' -import { SVGImages } from '@talent-connect/shared-atomic-design-components' import classnames from 'classnames' import './SignUpLanding.scss' -const SignUpLanding = () => { +function SignUpLanding() { const [selectedType, setSelectedType] = useState('') const history = useHistory() - const renderType = (name: string) => { - const type = name.toLowerCase() as SVGImages + const renderType = (name: 'Mentee' | 'Mentor') => { + const type = name.toLowerCase() as Lowercase return (
setSelectedType(type)} > @@ -51,7 +50,7 @@ const SignUpLanding = () => { Do you want to become a mentor or a{' '} mentee? -
+
{renderType('Mentee')} {renderType('Mentor')}
diff --git a/apps/redi-connect/src/redux/epics.ts b/apps/redi-connect/src/redux/epics.ts index 10d01f1b5..95bf42050 100644 --- a/apps/redi-connect/src/redux/epics.ts +++ b/apps/redi-connect/src/redux/epics.ts @@ -4,10 +4,11 @@ import { userEpics } from './user/epics' import { matchesEpics } from './matches/epics' import { mentoringSessionsEpics } from './mentoringSessions/epics' import { profilesEpics } from './profiles/epics' +import { objectValues } from '@talent-connect/typescript-utilities'; export const rootEpic = combineEpics( - ...Object.values(userEpics), - ...Object.values(matchesEpics), - ...Object.values(mentoringSessionsEpics), - ...Object.values(profilesEpics) + ...objectValues(userEpics), + ...objectValues(matchesEpics), + ...objectValues(mentoringSessionsEpics), + ...objectValues(profilesEpics) ) diff --git a/apps/redi-connect/src/redux/matches/epics.ts b/apps/redi-connect/src/redux/matches/epics.ts index 3dee161dc..c2929b1ea 100644 --- a/apps/redi-connect/src/redux/matches/epics.ts +++ b/apps/redi-connect/src/redux/matches/epics.ts @@ -7,7 +7,6 @@ import { profilesFetchOneStart } from '../profiles/actions' import { profileFetchStart } from '../user/actions' import { matchesAcceptMentorshipSuccess, - matchesMarkAsComplete, matchesFetchStart, matchesFetchSuccess, } from './actions' @@ -30,7 +29,7 @@ export const matchesFetchEpic = (action$: ActionsObservable) => switchMap(() => http(`${API_URL}/redMatches?filter=${JSON.stringify(fetchFilter)}`) ), - map((resp) => resp.data), + map(({ data }) => data), map(matchesFetchSuccess) ) @@ -39,42 +38,38 @@ export const matchesMarkAsDismissed = ( ) => action$.pipe( ofType(MatchesActionType.MATCHES_MARK_AS_DISMISSED_START), - switchMap((action) => + switchMap(({ payload }: MatchesMarkAsDismissedStartAction) => http(`${API_URL}/redMatches/markAsDismissed`, { method: 'post', data: { - redMatchId: (action as MatchesMarkAsDismissedStartAction).payload - .redMatchId, + redMatchId: payload.redMatchId, }, }) ), - map((resp) => resp.data), + map(({ data }) => data), map(matchesFetchStart) ) export const matchesAcceptMentorshipEpic = (action$: ActionsObservable) => action$.pipe( ofType(MatchesActionType.MATCHES_ACCEPT_MENTORSHIP_START), - switchMap((action) => { + switchMap(({ payload }: MatchesAcceptMentorshipStartAction) => { const request = from( http(`${API_URL}/redMatches/acceptMentorship`, { method: 'post', data: { - redMatchId: (action as MatchesAcceptMentorshipStartAction).payload - .redMatchId, - mentorReplyMessageOnAccept: ( - action as MatchesAcceptMentorshipStartAction - ).payload.mentorReplyMessageOnAccept, + redMatchId: payload.redMatchId, + mentorReplyMessageOnAccept: payload.mentorReplyMessageOnAccept, }, }) ).pipe( - map((resp) => resp.data), + map(({ data }) => data), map(matchesAcceptMentorshipSuccess) ) return request }), - switchMap((successAction: any) => { + switchMap((successAction) => { return concat( of(successAction), of(matchesFetchStart()), @@ -98,32 +93,25 @@ export const matchesDeclineMentorshipEpic = (action$: ActionsObservable) => action$.pipe( tap((p) => console.log('Hello hello', p)), ofType(MatchesActionType.MATCHES_DECLINE_MENTORSHIP_START), - switchMap((action) => { + switchMap(({ payload }: MatchesDeclineMentorshipStartAction) => { const request = from( http(`${API_URL}/redMatches/declineMentorship`, { method: 'post', data: { - redMatchId: (action as MatchesDeclineMentorshipStartAction).payload - .redMatchId, - ifDeclinedByMentor_chosenReasonForDecline: ( - action as MatchesDeclineMentorshipStartAction - ).payload.ifDeclinedByMentor_chosenReasonForDecline, - ifDeclinedByMentor_ifReasonIsOther_freeText: ( - action as MatchesDeclineMentorshipStartAction - ).payload.ifDeclinedByMentor_ifReasonIsOther_freeText, - ifDeclinedByMentor_optionalMessageToMentee: ( - action as MatchesDeclineMentorshipStartAction - ).payload.ifDeclinedByMentor_optionalMessageToMentee, + redMatchId: payload.redMatchId, + ifDeclinedByMentor_chosenReasonForDecline: payload.ifDeclinedByMentor_chosenReasonForDecline, + ifDeclinedByMentor_ifReasonIsOther_freeText: payload.ifDeclinedByMentor_ifReasonIsOther_freeText, + ifDeclinedByMentor_optionalMessageToMentee: payload.ifDeclinedByMentor_optionalMessageToMentee, }, }) ).pipe( - map((resp) => resp.data), + map(({ data }) => data), map(matchesAcceptMentorshipSuccess) ) return request }), - switchMap((successAction: any) => { + switchMap((successAction) => { return concat( of(successAction), of(matchesFetchStart()), @@ -146,25 +134,23 @@ export const matchesDeclineMentorshipEpic = (action$: ActionsObservable) => export const matchesMarkAsCompleteEpic = (action$: ActionsObservable) => action$.pipe( ofType(MatchesActionType.MATCHES_MARK_AS_COMPLETED), - switchMap((action) => { + switchMap(({ payload }: MatchesMarkAsCompleteAction) => { const request = from( http(`${API_URL}/redMatches/markAsCompleted`, { method: 'post', data: { - redMatchId: (action as MatchesMarkAsCompleteAction).payload - .redMatchId, - mentorMessageOnComplete: (action as MatchesMarkAsCompleteAction) - .payload.mentorMessageOnComplete, + redMatchId: payload.redMatchId, + mentorMessageOnComplete: payload.mentorMessageOnComplete, }, }) ).pipe( - map((resp) => resp.data), + map(({ data }) => data), map(matchesAcceptMentorshipSuccess) ) return request }), - switchMap((successAction: any) => { + switchMap((successAction) => { return concat( of(successAction), of(matchesFetchStart()), diff --git a/apps/redi-connect/src/redux/mentoringSessions/epics.ts b/apps/redi-connect/src/redux/mentoringSessions/epics.ts index 4304f8a64..85e6c21ae 100644 --- a/apps/redi-connect/src/redux/mentoringSessions/epics.ts +++ b/apps/redi-connect/src/redux/mentoringSessions/epics.ts @@ -46,7 +46,7 @@ export const mentoringSessionsCreateEpic = (action$: ActionsObservable) => return request }), - switchMap((successAction: any) => { + switchMap((successAction) => { return concat( of(successAction), of(mentoringSessionsFetchStart()), diff --git a/apps/redi-connect/src/redux/profiles/epics.ts b/apps/redi-connect/src/redux/profiles/epics.ts index 0de027ee4..9ec2f7487 100644 --- a/apps/redi-connect/src/redux/profiles/epics.ts +++ b/apps/redi-connect/src/redux/profiles/epics.ts @@ -11,7 +11,7 @@ export const profilesFetchOneEpic = ( action$.pipe( ofType(ProfilesActionType.PROFILES_FETCH_ONE_START), switchMap(({ payload }) => http(`${API_URL}/redProfiles/${payload}`)), - map((resp) => resp.data), + map(({ data }) => data), map(profilesFetchOneSuccess) ) diff --git a/apps/redi-connect/src/redux/profiles/reducer.ts b/apps/redi-connect/src/redux/profiles/reducer.ts index 565c6acbc..238cde2b4 100644 --- a/apps/redi-connect/src/redux/profiles/reducer.ts +++ b/apps/redi-connect/src/redux/profiles/reducer.ts @@ -1,7 +1,7 @@ import { ProfilesState, ProfilesActions, ProfilesActionType } from './types' const initialState: ProfilesState = { - oneProfile: undefined, + oneProfile: null, loading: false, } diff --git a/apps/redi-connect/src/redux/store.ts b/apps/redi-connect/src/redux/store.ts index 40f027efd..fa62024d2 100644 --- a/apps/redi-connect/src/redux/store.ts +++ b/apps/redi-connect/src/redux/store.ts @@ -1,17 +1,25 @@ -import { createStore, applyMiddleware, compose } from 'redux' +import { createStore, applyMiddleware, compose, StoreEnhancer } from 'redux' import { createEpicMiddleware } from 'redux-observable' import { rootEpic } from './epics' import { rootReducer } from './reducers' +declare global { + interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: ({ + trace: boolean, + traceLimit: number, + }) => (arg: StoreEnhancer) => StoreEnhancer; + } +} + // TODO: 'as Options' is a cheap way out, fix this const epicMiddleware = createEpicMiddleware() const composeEnhancers = - ((window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ && - (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ - trace: true, - traceLimit: 50, - })) || + window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__?.({ + trace: true, + traceLimit: 50, + }) || compose export const store = createStore( @@ -19,4 +27,4 @@ export const store = createStore( composeEnhancers(applyMiddleware(epicMiddleware)) ) -epicMiddleware.run(rootEpic as any) +epicMiddleware.run(rootEpic) diff --git a/apps/redi-connect/src/redux/user/epics.ts b/apps/redi-connect/src/redux/user/epics.ts index 35c26d20f..f214905e7 100644 --- a/apps/redi-connect/src/redux/user/epics.ts +++ b/apps/redi-connect/src/redux/user/epics.ts @@ -15,8 +15,7 @@ const profileFetchEpic = (action$: ActionsObservable) => filter(() => isLoggedIn()), switchMap(async () => { try { - const res = await fetchSaveRedProfile(getAccessTokenFromLocalStorage()) - return res + return fetchSaveRedProfile(getAccessTokenFromLocalStorage()) } catch (err) { return of(err) } diff --git a/apps/redi-connect/src/redux/user/selectors.ts b/apps/redi-connect/src/redux/user/selectors.ts index e9025abb0..beff296c8 100644 --- a/apps/redi-connect/src/redux/user/selectors.ts +++ b/apps/redi-connect/src/redux/user/selectors.ts @@ -3,5 +3,5 @@ import { UserState } from './types' export const getHasReachedMenteeLimit = (state: UserState) => { const profile = state.profile if (!profile) return false - return profile.currentFreeMenteeSpots === 0 + return !profile.currentFreeMenteeSpots } diff --git a/apps/redi-connect/src/services/api/api.tsx b/apps/redi-connect/src/services/api/api.tsx index 48853f9e1..ef403c749 100644 --- a/apps/redi-connect/src/services/api/api.tsx +++ b/apps/redi-connect/src/services/api/api.tsx @@ -28,15 +28,14 @@ export const signUp = async ( email = email.toLowerCase() const rediLocation = process.env.NX_REDI_CONNECT_REDI_LOCATION as RediLocation redProfile.rediLocation = rediLocation - const userResponse = await http(`${API_URL}/redUsers`, { + const { data: user } = await http(`${API_URL}/redUsers`, { method: 'post', data: { email, password, rediLocation }, }) - const user = userResponse.data as RedUser - saveRedUserToLocalStorage(user) + saveRedUserToLocalStorage(user as RedUser) const accessToken = await login(email, password) saveAccessTokenToLocalStorage(accessToken) - const profileResponse = await http( + const { data: profile } = await http( `${API_URL}/redUsers/${user.id}/redProfile`, { method: 'post', @@ -46,24 +45,19 @@ export const signUp = async ( }, } ) - const profile = profileResponse.data as RedProfile - localStorageSaveRedProfile(profile) + localStorageSaveRedProfile(profile as RedProfile) } export const login = async ( email: string, password: string ): Promise => { - email = email.toLowerCase() - const loginResp = await http(`${API_URL}/redUsers/login`, { + const { data } = await http(`${API_URL}/redUsers/login`, { method: 'post', - data: { email, password }, - headers: { - RedProduct: 'CON', - }, + data: { email: email.toLowerCase(), password }, + headers: { RedProduct: 'CON' }, }) - const accessToken = loginResp.data as AccessToken - return accessToken + return data } export const logout = () => { @@ -71,11 +65,10 @@ export const logout = () => { history.push('/front/home') } -export const requestResetPasswordEmail = async (email: string) => { - email = email.toLowerCase() - await axios(`${API_URL}/redUsers/requestResetPasswordEmail`, { +export async function requestResetPasswordEmail (email: string): Promise { + return axios(`${API_URL}/redUsers/requestResetPasswordEmail`, { method: 'post', - data: { email, redproduct: 'CON' }, + data: { email: email.toLowerCase(), redproduct: 'CON' }, }) } @@ -87,14 +80,9 @@ export const setPassword = async (password: string) => { }) } -export const fetchSaveRedProfile = async ( - accessToken: AccessToken -): Promise => { - const { userId, id: token } = accessToken +export const fetchSaveRedProfile = async ({ userId, id: token }: AccessToken): Promise => { const profileResp = await http(`${API_URL}/redUsers/${userId}/redProfile`, { - headers: { - Authorization: token, - }, + headers: { Authorization: token, }, }) try { const profile = profileResp.data as RedProfile @@ -109,8 +97,7 @@ export const fetchSaveRedProfile = async ( export const saveRedProfile = async ( redProfile: Partial ): Promise => { - const id = redProfile.id - const saveProfileResp = await http(`${API_URL}/redProfiles/${id}`, { + const saveProfileResp = await http(`${API_URL}/redProfiles/${redProfile.id}`, { data: redProfile, method: 'patch', }) @@ -135,11 +122,11 @@ export const getProfiles = ({ nameQuery, }: RedProfileFilters): Promise => { const filterLanguages = - languages && languages.length !== 0 ? { inq: languages } : undefined + languages?.length ? { inq: languages } : null const filterCategories = - categories && categories.length !== 0 ? { inq: categories } : undefined + categories?.length ? { inq: categories } : null const filterLocations = - locations && locations.length !== 0 ? { inq: locations } : undefined + locations?.length ? { inq: locations } : null return http( `${API_URL}/redProfiles?filter=${JSON.stringify({ @@ -190,31 +177,34 @@ export const getProfile = (profileId: string): Promise => http(`${API_URL}/redProfiles/${profileId}`).then((resp) => resp.data) // TODO: status: 'applied' here should be matched against RedMatch['status'] -export const fetchApplicants = async (): Promise => - http( +export async function fetchApplicants (): Promise { + return (await http( `${API_URL}/redMatches?filter=` + JSON.stringify({ where: { mentorId: getRedProfileFromLocalStorage().id, status: 'applied', }, - }) - ).then((resp) => resp.data) + }))) + .data +} -export const requestMentorship = ( +export async function requestMentorship ( applicationText: string, expectationText: string, mentorId: string -): Promise => - http(`${API_URL}/redMatches/requestMentorship`, { +): Promise { + return (await http(`${API_URL}/redMatches/requestMentorship`, { method: 'post', data: { applicationText, expectationText, mentorId }, - }).then((resp) => resp.data) + })) + .data +} -export const reportProblem = async ( - problemReport: RedProblemReportDto -): Promise => - http(`${API_URL}/redProblemReports`, { +export async function reportProblem (problemReport: RedProblemReportDto): Promise { + const { data } = await http(`${API_URL}/redProblemReports`, { method: 'post', data: problemReport, - }).then((resp) => resp.data) + }) + return data +} diff --git a/apps/redi-connect/src/services/auth/auth.ts b/apps/redi-connect/src/services/auth/auth.ts index 506b4e1c1..a7296d982 100644 --- a/apps/redi-connect/src/services/auth/auth.ts +++ b/apps/redi-connect/src/services/auth/auth.ts @@ -3,38 +3,39 @@ import { AccessToken } from '@talent-connect/shared-types' import { RedProfile } from '@talent-connect/shared-types' export const isLoggedIn = (): boolean => { - const profile: any = window.localStorage.getItem('redProfile') - const accessToken: any = window.localStorage.getItem('accessToken') + const profile = window.localStorage.getItem('redProfile') + const accessToken = window.localStorage.getItem('accessToken') try { - const r1: any = JSON.parse(profile) - const r2: any = JSON.parse(accessToken) - return r1 && r2 + return !!JSON.parse(profile) && !!JSON.parse(accessToken) } catch (err) { return false } } -export const getRedUserFromLocalStorage = (): RedUser => - JSON.parse(window.localStorage.getItem('redUser') as string) +export function getRedUserFromLocalStorage (): RedUser { + return JSON.parse(window.localStorage.getItem('redUser')) +} -export const saveRedUserToLocalStorage = (redUser: RedUser) => { - window.localStorage.setItem('redUser', JSON.stringify(redUser)) +export function saveRedUserToLocalStorage (redUser: RedUser): void { + return window.localStorage.setItem('redUser', JSON.stringify(redUser)) } -export const getRedProfileFromLocalStorage = (): RedProfile => - JSON.parse(window.localStorage.getItem('redProfile') as string) +export function getRedProfileFromLocalStorage (): RedProfile { + return JSON.parse(window.localStorage.getItem('redProfile')) +} -export const saveRedProfileToLocalStorage = (redProfile: RedProfile) => { +export function saveRedProfileToLocalStorage (redProfile: RedProfile): void { window.localStorage.setItem('redProfile', JSON.stringify(redProfile)) } -export const getAccessTokenFromLocalStorage = (): AccessToken => - JSON.parse(window.localStorage.getItem('accessToken') as string) +export function getAccessTokenFromLocalStorage (): AccessToken { + return JSON.parse(window.localStorage.getItem('accessToken')) +} -export const saveAccessTokenToLocalStorage = (accessToken: AccessToken) => { +export function saveAccessTokenToLocalStorage (accessToken: AccessToken) { window.localStorage.setItem('accessToken', JSON.stringify(accessToken)) } -export const purgeAllSessionData = () => { +export function purgeAllSessionData (): void { window.localStorage.clear() } diff --git a/apps/redi-connect/src/services/history/history.tsx b/apps/redi-connect/src/services/history/history.tsx index 88f13f9ca..40de1a6f3 100644 --- a/apps/redi-connect/src/services/history/history.tsx +++ b/apps/redi-connect/src/services/history/history.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import { createContext } from 'react' import { Router } from 'react-router-dom' import { createBrowserHistory } from 'history' @@ -18,7 +18,7 @@ const history = createBrowserHistory() // } // }) -export const HistoryContext = React.createContext(history) +export const HistoryContext = createContext(history) export { history, Router } diff --git a/apps/redi-connect/src/services/http/http.tsx b/apps/redi-connect/src/services/http/http.tsx index 89b64dc74..879a5c878 100644 --- a/apps/redi-connect/src/services/http/http.tsx +++ b/apps/redi-connect/src/services/http/http.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import { createContext } from 'react' import axios from 'axios' import has from 'lodash/has' import includes from 'lodash/includes' @@ -19,8 +19,8 @@ http.interceptors.request.use( const isAuthorizationHeaderSet = has(config, 'headers.Authorization') const _isLoggedIn = isLoggedIn() if (_isLoggedIn && !isAuthorizationHeaderSet) { - const accessToken = getAccessTokenFromLocalStorage() - config.headers.Authorization = `${accessToken.id}` + const { id } = getAccessTokenFromLocalStorage() + config.headers.Authorization = id } return config }, @@ -44,16 +44,14 @@ http.interceptors.response.use( if (includes([401, 403], err.response.status)) { purgeAllSessionData() - history.push( - `/front/login?goto=${encodeURIComponent(history.location.pathname)}` - ) + history.push(`/front/login?goto=${encodeURIComponent(history.location.pathname)}`) } else { history.push('/error/4xx') } } ) -export const HttpContext = React.createContext(http) +export const HttpContext = createContext(http) /* export function with$Http(Component) { diff --git a/apps/redi-connect/src/styles/_global.scss b/apps/redi-connect/src/styles/_global.scss index 8e1cdbc18..5c87752cd 100644 --- a/apps/redi-connect/src/styles/_global.scss +++ b/apps/redi-connect/src/styles/_global.scss @@ -1,3 +1,4 @@ +@use '_colors' as colors; @import '~bulma/sass/utilities/_all'; @import './_variables.scss'; @@ -6,13 +7,13 @@ @include mobile() { padding-bottom: 1.5rem; margin-bottom: -0.5rem; - border-bottom: 1px solid $grey-light; + border-bottom: 1px solid colors.$grey-light; } } &:not(:last-child) { padding-bottom: 1.5rem; margin-bottom: 1.5rem; - border-bottom: 1px solid $grey-light; + border-bottom: 1px solid colors.$grey-light; } } diff --git a/apps/redi-connect/src/styles/main.scss b/apps/redi-connect/src/styles/main.scss index 2cff598d8..af7cf4712 100644 --- a/apps/redi-connect/src/styles/main.scss +++ b/apps/redi-connect/src/styles/main.scss @@ -1,4 +1,5 @@ // custom variable overwrites for bulma +@use '_colors' as colors; @import '_variables.scss'; @import '_global.scss'; @@ -32,7 +33,7 @@ code { } %arrow { - border: 2px solid $redi-orange-dark; + border: 2px solid colors.$redi-orange-dark; border-right: 0; border-top: 0; content: ' '; @@ -52,7 +53,7 @@ textarea, select { &::placeholder { font-style: italic; - color: $grey !important; + color: colors.$grey !important; } } @@ -78,10 +79,10 @@ a:hover { } &--separator { - border-top: 1px solid $grey-light; + border-top: 1px solid colors.$grey-light; } - &.section--bottom-large-spaceing { + &.section--bottom-large-spacing { padding-bottom: 4rem; @include tablet { @@ -110,18 +111,18 @@ a:hover { // TODO: should be transfered to a layout template // will be done the next time this view is reused .section.color-half-desktop { - background-color: $white; + background-color: colors.$white; @include tablet() { - background: linear-gradient(90deg, $redi-blue-light 50%, $white 50%); + background: linear-gradient(90deg, colors.$redi-blue-light 50%, colors.$white 50%); } } .section.color-half-tablet { - background-color: $white; + background-color: colors.$white; @include desktop() { - background: linear-gradient(90deg, $redi-blue-light 50%, $white 50%); + background: linear-gradient(90deg, colors.$redi-blue-light 50%, colors.$white 50%); } } @@ -137,7 +138,7 @@ a:hover { @include desktop() { margin: -3.25rem auto; padding: 3.25rem 0; - background: linear-gradient(90deg, $redi-blue-light 15.8%, $white 15.8%); + background: linear-gradient(90deg, colors.$redi-blue-light 15.8%, colors.$white 15.8%); } } diff --git a/apps/redi-connect/src/utils/yup-error-to-simple-object.ts b/apps/redi-connect/src/utils/yup-error-to-simple-object.ts index 9995ec677..463489630 100644 --- a/apps/redi-connect/src/utils/yup-error-to-simple-object.ts +++ b/apps/redi-connect/src/utils/yup-error-to-simple-object.ts @@ -1,8 +1,9 @@ export type ErrorSimpleObject = { [key: string]: string } +// TODO: repeated export const yupErrorToSimpleObject = (error: any): ErrorSimpleObject => { const targetErrorsObject: ErrorSimpleObject = {} - error.inner.forEach((error: any) => { + error.inner.forEach((error) => { const fieldName: string = error.path const fieldError: string = error.message targetErrorsObject[fieldName] = fieldError diff --git a/apps/redi-talent-pool/src/components/AppNotification.tsx b/apps/redi-talent-pool/src/components/AppNotification.tsx index b2d0b5f7c..1620f682b 100644 --- a/apps/redi-talent-pool/src/components/AppNotification.tsx +++ b/apps/redi-talent-pool/src/components/AppNotification.tsx @@ -13,7 +13,7 @@ import ErrorIcon from '@material-ui/icons/Error' import InfoIcon from '@material-ui/icons/Info' import WarningIcon from '@material-ui/icons/Warning' import clsx from 'clsx' -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { Subject } from 'rxjs' import { Optional } from 'utility-types' @@ -58,7 +58,7 @@ const useNotificationStyles = makeStyles((theme: Theme) => ({ export interface AppNotificationOptions { variant: NotificationVariant - autoHideDuration: number | undefined + autoHideDuration: number | null } interface SubjectShowNotification extends AppNotificationOptions { message: string | null @@ -116,12 +116,9 @@ export function AppNotification() { className={state ? styleClasses[state.variant] : ''} message={ - {Icon && ( - - )} - {state && state.message} + {Icon && + } + {state?.message} } action={[ diff --git a/apps/redi-talent-pool/src/components/PrivateRoute.tsx b/apps/redi-talent-pool/src/components/PrivateRoute.tsx index 2062555b9..d275a3925 100644 --- a/apps/redi-talent-pool/src/components/PrivateRoute.tsx +++ b/apps/redi-talent-pool/src/components/PrivateRoute.tsx @@ -1,8 +1,7 @@ -import React, { FunctionComponent } from 'react' import { Route, RouteProps } from 'react-router-dom' import { useNotAuthenticatedRedirector } from '../hooks/useNotAuthenticatedRedirector' -export const PrivateRoute: FunctionComponent = (props) => { +export function PrivateRoute (props: RouteProps) { const { isRedirectingToLogin } = useNotAuthenticatedRedirector() // If the hook determined user not authenticated it'll take care of redirect to diff --git a/apps/redi-talent-pool/src/components/Routes.tsx b/apps/redi-talent-pool/src/components/Routes.tsx index 2e6a01c12..28aab5b62 100644 --- a/apps/redi-talent-pool/src/components/Routes.tsx +++ b/apps/redi-talent-pool/src/components/Routes.tsx @@ -1,24 +1,18 @@ -import React from 'react' import { Redirect, Route, Switch } from 'react-router-dom' import { allRoutes } from '../routes/index' import { PrivateRoute } from './PrivateRoute' -export const Routes = () => ( -
- - {allRoutes.map(({ requiresLoggedIn, exact, path, component }, i) => - requiresLoggedIn ? ( - - ) : ( - - ) - )} - - -
-) +export function Routes () { + return ( +
+ + {allRoutes.map(({ requiresLoggedIn, exact, path, component }, i) => + requiresLoggedIn + ? () + : () + )} + + +
+ ); +} diff --git a/apps/redi-talent-pool/src/components/molecules/AccordionForm.scss b/apps/redi-talent-pool/src/components/molecules/AccordionForm.scss index a158a6031..9bdb64a02 100644 --- a/apps/redi-talent-pool/src/components/molecules/AccordionForm.scss +++ b/apps/redi-talent-pool/src/components/molecules/AccordionForm.scss @@ -1,9 +1,10 @@ +@use '_colors' as colors; @import '_variables.scss'; .accordion-form { padding-top: 1rem; // margin-bottom: 0.75rem; - border-bottom: 1px solid $grey-light; + border-bottom: 1px solid colors.$grey-light; &-block__title { font-weight: $weight-bold; diff --git a/apps/redi-talent-pool/src/components/molecules/AccordionForm.tsx b/apps/redi-talent-pool/src/components/molecules/AccordionForm.tsx index fe2b45e14..5d210f274 100644 --- a/apps/redi-talent-pool/src/components/molecules/AccordionForm.tsx +++ b/apps/redi-talent-pool/src/components/molecules/AccordionForm.tsx @@ -1,10 +1,9 @@ import { - Button, Caption, Icon, } from '@talent-connect/shared-atomic-design-components' import classnames from 'clsx' -import React, { useEffect, useState } from 'react' +import { ReactNode, useEffect, useState } from 'react' import { Columns, Element } from 'react-bulma-components' import { Subject } from 'rxjs' import './AccordionForm.scss' @@ -13,14 +12,14 @@ interface Props { title: string isSaveDisabled?: boolean closeAccordionSignalSubject?: Subject - children: React.ReactNode + children: ReactNode } -export function AccordionForm({ +export function AccordionForm ({ title, closeAccordionSignalSubject, children, -}) { +}: Props) { const [isOpen, setIsOpen] = useState(false) useEffect(() => { diff --git a/apps/redi-talent-pool/src/components/molecules/CVDownloadButton.tsx b/apps/redi-talent-pool/src/components/molecules/CVDownloadButton.tsx index 62e016317..b54b1bdc3 100644 --- a/apps/redi-talent-pool/src/components/molecules/CVDownloadButton.tsx +++ b/apps/redi-talent-pool/src/components/molecules/CVDownloadButton.tsx @@ -1,6 +1,6 @@ import { PDFDownloadLink, StyleSheet } from '@react-pdf/renderer' import { Button } from '@talent-connect/shared-atomic-design-components' -import React from 'react' +import { TpJobSeekerCv } from '@talent-connect/shared-types'; import { CVPDF } from './CvPdfPreview' // Create styles @@ -10,10 +10,10 @@ const styles = StyleSheet.create({ interface CVDownloadButtonProps { // cvData: UserCVData - cvData: any + cvData: TpJobSeekerCv } -const CVDownloadButton = ({ cvData }: CVDownloadButtonProps) => { +function CVDownloadButton ({ cvData }: CVDownloadButtonProps) { return ( 43 } -function isVeryLongEducationLine(education) { - return ( - education?.type?.length || 0 + education?.institutionName?.length || 0 > 43 - ) +function isVeryLongEducationLine(education) { // TODO: review + return education?.type?.length || 0 + education?.institutionName?.length || 0 > 43 +} + +interface CVPDFProps { + cvData: Partial; } -export const CVPDF = ({ +export function CVPDF ({ cvData: { firstName, lastName, @@ -265,13 +268,11 @@ export const CVPDF = ({ aboutYourself, topSkills, workingLanguages, - projects, experience, education, phoneNumber, contactEmail, postalMailingAddress, - personalWebsite, githubUrl, linkedInUrl, @@ -280,9 +281,7 @@ export const CVPDF = ({ stackOverflowUrl, dribbbleUrl, }, -}: { - cvData: Partial -}) => { +}: CVPDFProps) { return ( @@ -300,7 +299,7 @@ export const CVPDF = ({ styles.headerText2, desiredPositions?.length > 2 ? styles.headerText2ExtraTop - : undefined, + : null, ]} > {firstName} @@ -310,7 +309,7 @@ export const CVPDF = ({ styles.headerText3, desiredPositions?.length > 2 ? styles.headerText3ExtraTop - : undefined, + : null, ]} > {lastName} @@ -347,8 +346,7 @@ export const CVPDF = ({ Languages - {workingLanguages?.map( - ({ language, proficiencyLevelId }, index) => ( + {workingLanguages?.map(({ language, proficiencyLevelId }, index) => ( Work Experience {experience - ?.filter((item) => { - const { uuid, ...all } = item - const vals = Object.values(all) - return vals.some((val) => val) - }) + ?.filter(({ uuid, ...all }) => objectValues(all).some((val) => val)) .map((experience, index) => ( Education {education - ?.filter((item) => { - const { uuid, ...all } = item - const vals = Object.values(all) - return vals.some((val) => val) - }) + ?.filter(({ uuid, ...all }) => objectValues(all).some((val) => val)) .map((education, index) => ( { return items .reduce((acc, curr) => (curr ? `${acc}\n${curr}` : acc), '') @@ -571,18 +550,17 @@ const getNodeTopPosition = (xPath: string) => { } interface CVPDFPreviewProps { - cvData: Partial + cvData: Partial pdfHeightPx: number pdfWidthPx: number } -export const CVPDFPreview = ({ +export function CVPDFPreview ({ cvData, pdfHeightPx, pdfWidthPx, -}: CVPDFPreviewProps) => +}: CVPDFPreviewProps) { //pdfWidthPx: number - { const [instance, updateInstance] = usePDF({ document: , }) @@ -639,7 +617,7 @@ export const CVPDFPreview = ({ ) } -export const CVPDFPreviewMemoized = React.memo( +export const CVPDFPreviewMemoized = memo( CVPDFPreview, (prevProps, nextProps) => isEqual(prevProps, nextProps) ) diff --git a/apps/redi-talent-pool/src/components/molecules/Editable.scss b/apps/redi-talent-pool/src/components/molecules/Editable.scss index 94f869a31..8d2bae255 100644 --- a/apps/redi-talent-pool/src/components/molecules/Editable.scss +++ b/apps/redi-talent-pool/src/components/molecules/Editable.scss @@ -1,9 +1,9 @@ -@import '_variables.scss'; +@use '_colors' as colors; .profile-section { &--title { padding-bottom: 0.5rem; - border-bottom: 1px solid $grey-light; + border-bottom: 1px solid colors.$grey-light; margin-bottom: 1rem; } diff --git a/apps/redi-talent-pool/src/components/molecules/Editable.tsx b/apps/redi-talent-pool/src/components/molecules/Editable.tsx index 5c14d1d45..c72c19be5 100644 --- a/apps/redi-talent-pool/src/components/molecules/Editable.tsx +++ b/apps/redi-talent-pool/src/components/molecules/Editable.tsx @@ -3,24 +3,24 @@ import { Icon, Modal, } from '@talent-connect/shared-atomic-design-components' -import React from 'react' +import { CSSProperties, ReactNode } from 'react' import { Element } from 'react-bulma-components' import './Editable.scss' interface Props { isEditing: boolean - setIsEditing: (boolean) => void + setIsEditing: (boolean: boolean) => void disableEditing?: boolean isFormDirty?: boolean title?: string - readComponent: React.ReactNode + readComponent: ReactNode modalTitle: string modalHeadline: string - modalBody: React.ReactNode - modalStyles?: React.CSSProperties + modalBody: ReactNode + modalStyles?: CSSProperties } -export function Editable({ +export function Editable ({ isEditing, setIsEditing, disableEditing, @@ -45,26 +45,24 @@ export function Editable({ > {title} - {!disableEditing ? ( + {!disableEditing && (
setIsEditing(true)}>
- ) : null} + )}
) : (
  - {!disableEditing ? ( -
setIsEditing(true)} - style={{ position: 'relative', top: '50px' }} - > - -
- ) : ( -
- )} + {!disableEditing + ? (
setIsEditing(true)} + style={{ position: 'relative', top: '50px' }} + > + +
) + : (
)}
)} diff --git a/apps/redi-talent-pool/src/components/molecules/EmptySectionPlaceholder.tsx b/apps/redi-talent-pool/src/components/molecules/EmptySectionPlaceholder.tsx index db693fd85..36aed75ad 100644 --- a/apps/redi-talent-pool/src/components/molecules/EmptySectionPlaceholder.tsx +++ b/apps/redi-talent-pool/src/components/molecules/EmptySectionPlaceholder.tsx @@ -1,14 +1,15 @@ import './EmptySectionPlaceholder.scss' import classnames from 'clsx' +import { CSSProperties, ReactNode } from 'react'; interface Props { height: 'extra-slim' | 'slim' | 'tall' | 'none' onClick: () => void - style?: React.CSSProperties - children: React.ReactNode + style?: CSSProperties + children: ReactNode } -export function EmptySectionPlaceholder({ +export function EmptySectionPlaceholder ({ height, onClick, style = {}, @@ -24,5 +25,5 @@ export function EmptySectionPlaceholder({ > {children}
- ) -} + ); +} \ No newline at end of file diff --git a/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.scss b/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.scss index 0e43079f4..0d2c79d61 100644 --- a/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.scss +++ b/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.scss @@ -23,41 +23,44 @@ &__profile-page { background-image: url('../../assets/images/navigation-icons/profile-page__idle.svg'); - } - &__profile-page:hover { - background-image: url('../../assets/images/navigation-icons/profile-page__hover.svg'); - } - &__profile-page:active { - background-image: url('../../assets/images/navigation-icons/profile-page__mousedown.svg'); - } - &__profile-page.disabled { - background-image: url('../../assets/images/navigation-icons/profile-page__disabled.svg'); + + &:hover { + background-image: url('../../assets/images/navigation-icons/profile-page__hover.svg'); + } + &:active { + background-image: url('../../assets/images/navigation-icons/profile-page__mousedown.svg'); + } + &.disabled { + background-image: url('../../assets/images/navigation-icons/profile-page__disabled.svg'); + } } &__browse-page { background-image: url('../../assets/images/navigation-icons/browse-page__idle.svg'); - } - &__browse-page:hover { - background-image: url('../../assets/images/navigation-icons/browse-page__hover.svg'); - } - &__browse-page:active { - background-image: url('../../assets/images/navigation-icons/browse-page__mousedown.svg'); - } - &__browse-page.disabled { - background-image: url('../../assets/images/navigation-icons/browse-page__disabled.svg'); + + &:hover { + background-image: url('../../assets/images/navigation-icons/browse-page__hover.svg'); + } + &:active { + background-image: url('../../assets/images/navigation-icons/browse-page__mousedown.svg'); + } + &.disabled { + background-image: url('../../assets/images/navigation-icons/browse-page__disabled.svg'); + } } &__cv-builder-page { background-image: url('../../assets/images/navigation-icons/cv-builder-page__idle.svg'); - } - &__cv-builder-page:hover { - background-image: url('../../assets/images/navigation-icons/cv-builder-page__hover.svg'); - } - &__cv-builder-page:active { - background-image: url('../../assets/images/navigation-icons/cv-builder-page__mousedown.svg'); - } - &__cv-builder-page.disabled { - background-image: url('../../assets/images/navigation-icons/cv-builder-page__disabled.svg'); + + &:hover { + background-image: url('../../assets/images/navigation-icons/cv-builder-page__hover.svg'); + } + &:active { + background-image: url('../../assets/images/navigation-icons/cv-builder-page__mousedown.svg'); + } + &.disabled { + background-image: url('../../assets/images/navigation-icons/cv-builder-page__disabled.svg'); + } } .disabled { diff --git a/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.tsx b/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.tsx index 149361511..89b87bee0 100644 --- a/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.tsx +++ b/apps/redi-talent-pool/src/components/molecules/TpMainNavItem.tsx @@ -1,12 +1,11 @@ -import { Tooltip } from '@material-ui/core' import classnames from 'clsx' -import React from 'react' +import React, { ReactNode, MouseEvent } from 'react' import { useCallback } from 'react' import { Link } from 'react-router-dom' import './TpMainNavItem.scss' interface Props { - page: 'profile-page' | 'browse-page' | 'cv-builder-page' + page: `${'profile' | 'browse' | 'cv-builder'}-page` to: string isActive?: boolean isDisabled?: boolean @@ -14,25 +13,24 @@ interface Props { } interface FancyLinkProps { - onClick: (event: React.MouseEvent) => void + onClick: (event: MouseEvent) => void isDisabled?: boolean - children?: React.ReactNode + children?: ReactNode } -const FancyLink = React.forwardRef( - (props: FancyLinkProps, ref) => ( +const FancyLink = React.forwardRef((props: FancyLinkProps, ref) => ( {props.children} ) ) -export function TpMainNavItem({ +export function TpMainNavItem ({ page, to, isActive, @@ -40,28 +38,23 @@ export function TpMainNavItem({ pageName, }: Props) { const onClick = useCallback( - (event: React.MouseEvent) => { - if (isDisabled) { - event.preventDefault() - } - }, + (event: MouseEvent) => { if (isDisabled) event.preventDefault() }, [isDisabled] ) return ( ( - + )} - onClick={onClick} >
- +
@@ -69,15 +62,17 @@ export function TpMainNavItem({ ) } -function TpMainNavItemIcon({ - page, - isDisabled, - pageName, -}: { +interface TpMainNavItemIconProps { page: string isDisabled?: boolean pageName?: string -}) { +} + +function TpMainNavItemIcon ({ + page, + isDisabled, + pageName, +}: TpMainNavItemIconProps) { return (
{ - return ( - <> - - - {children} - - - ) -} +// function TopIllustration ({ children }) { // TODO: remove? +// return ( +// <> +// +// +// {children} +// +// +// ) +// } // TODO: rename this component to Teaser, TpTeaser was an attempt at a fix, when it was another issue that was the issue export default { @@ -44,7 +43,7 @@ export default { Don't have an account yet?{' '} - signup here + signup here diff --git a/apps/redi-talent-pool/src/components/organisms/Avatar.form.ts b/apps/redi-talent-pool/src/components/organisms/Avatar.form.ts new file mode 100644 index 000000000..1faf0b69b --- /dev/null +++ b/apps/redi-talent-pool/src/components/organisms/Avatar.form.ts @@ -0,0 +1,22 @@ +import { createComponentForm } from '@talent-connect/shared-utils'; + +interface FormProps { + profileAvatarImageS3Key: string; +} + +export interface AvatarFormValues { + id: string; + profileAvatarImageS3Key: string; + profileSaveStart: (profile: FormProps & { id: string; }) => void; +} + +export const componentForm = createComponentForm() + .validation((yup) => ({ + /** */ + profileAvatarImageS3Key: yup.string() + .max(255), + })) + .initialValues((init) => init) + .onSubmit((data, actions, { profileSaveStart, id }) => { + profileSaveStart({ ...data, id }) + }) diff --git a/apps/redi-talent-pool/src/components/organisms/Avatar.scss b/apps/redi-talent-pool/src/components/organisms/Avatar.scss index 06ffe2ca5..b74362cb9 100644 --- a/apps/redi-talent-pool/src/components/organisms/Avatar.scss +++ b/apps/redi-talent-pool/src/components/organisms/Avatar.scss @@ -1,3 +1,4 @@ +@use '_colors' as colors; @import '~bulma/sass/utilities/_all'; @import '_variables.scss'; @@ -23,7 +24,7 @@ } &--editable { - border: 4px solid $redi-orange-dark; + border: 4px solid colors.$redi-orange-dark; box-shadow: 2px 0 15px 0 rgba(255, 92, 31, 0.34); padding-top: calc(100% - 8px); @@ -62,7 +63,7 @@ &--placeholder:before { border: 1px solid #a0a0a0; - background-color: $grey-extra-light; + background-color: colors.$grey-extra-light; } &__image { @@ -86,8 +87,8 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - border: 1px solid $grey-light; - color: $grey-light; + border: 1px solid colors.$grey-light; + color: colors.$grey-light; border-radius: $radius; } diff --git a/apps/redi-talent-pool/src/components/organisms/Avatar.tsx b/apps/redi-talent-pool/src/components/organisms/Avatar.tsx index e066f1bc2..731633ea4 100644 --- a/apps/redi-talent-pool/src/components/organisms/Avatar.tsx +++ b/apps/redi-talent-pool/src/components/organisms/Avatar.tsx @@ -1,85 +1,67 @@ +import { ReactNode } from 'react'; +import ReactS3Uploader, { S3Response } from 'react-s3-uploader' +import classnames from 'classnames' + import { AWS_PROFILE_AVATARS_BUCKET_BASE_URL, S3_UPLOAD_SIGN_URL, } from '@talent-connect/shared-config' import { - TpJobseekerProfile, + TpJobSeekerProfile, TpCompanyProfile, } from '@talent-connect/shared-types' -import classnames from 'classnames' -import { FormikValues, useFormik } from 'formik' import { Element } from 'react-bulma-components' -import ReactS3Uploader from 'react-s3-uploader' -import * as Yup from 'yup' import placeholderImage from '../../assets/img-placeholder.png' import { ReactComponent as UploadImage } from '../../assets/uploadImage.svg' import './Avatar.scss' +import { componentForm } from './Avatar.form' interface AvatarProps { - profile: Partial | Partial -} -interface AvatarEditable { - profile: Partial | Partial - profileSaveStart: ( - profile: Partial | Partial - ) => void - callToActionText?: string + profile: Partial | Partial } -interface AvatarFormValues { - profileAvatarImageS3Key: string -} - -const validationSchema = Yup.object({ - profileAvatarImageS3Key: Yup.string().max(255), -}) - -const Avatar = ({ profile }: AvatarProps) => { - const { profileAvatarImageS3Key } = profile +function Avatar ({ + profile: { profileAvatarImageS3Key, firstName, lastName } +}: AvatarProps) { const imgSrc = profileAvatarImageS3Key - ? AWS_PROFILE_AVATARS_BUCKET_BASE_URL + profileAvatarImageS3Key - : placeholderImage - + ? AWS_PROFILE_AVATARS_BUCKET_BASE_URL + profileAvatarImageS3Key + : placeholderImage + return (
{`${profile.firstName} + />
) } +interface AvatarEditableProps { + profile: Partial | Partial + profileSaveStart: (profile: Partial | Partial) => void + callToActionText?: string +} -const AvatarEditable = ({ - profile, +function AvatarEditable ({ + profile: { profileAvatarImageS3Key, id, firstName, lastName }, profileSaveStart, callToActionText = 'Add your picture', -}: AvatarEditable) => { - const { profileAvatarImageS3Key } = profile +}: AvatarEditableProps) { const imgURL = AWS_PROFILE_AVATARS_BUCKET_BASE_URL + profileAvatarImageS3Key - const submitForm = async (values: FormikValues) => { - const profileMe = values as Partial - profileSaveStart({ ...profileMe, id: profile.id }) - } - - const initialValues: AvatarFormValues = { - profileAvatarImageS3Key: profileAvatarImageS3Key, - } - - const formik = useFormik({ - initialValues: initialValues, - validationSchema, - onSubmit: submitForm, + const formik = componentForm({ + id, + profileAvatarImageS3Key, + profileSaveStart }) - const onUploadSuccess = (result: any) => { - formik.setFieldValue('profileAvatarImageS3Key', result.fileKey) + const onUploadSuccess = ({ fileKey }: S3Response) => { + formik.setFieldValue('profileAvatarImageS3Key', fileKey) formik.handleSubmit() } @@ -93,7 +75,7 @@ const AvatarEditable = ({ <> {`${profile.firstName} console.log(c)} - onError={(c: any) => console.log(c)} + onSignedUrl={(c) => console.log(c)} + onError={(c) => console.log(c)} onFinish={onUploadSuccess} contentDisposition="auto" /> @@ -136,7 +118,7 @@ const AvatarEditable = ({ ) } -Avatar.Some = (profile: TpJobseekerProfile) => +Avatar.Some = (profile: TpJobSeekerProfile) => Avatar.Editable = AvatarEditable export default Avatar diff --git a/apps/redi-talent-pool/src/components/organisms/Footer.scss b/apps/redi-talent-pool/src/components/organisms/Footer.scss index 7e00bb8c5..d3a1368e2 100644 --- a/apps/redi-talent-pool/src/components/organisms/Footer.scss +++ b/apps/redi-talent-pool/src/components/organisms/Footer.scss @@ -1,9 +1,10 @@ +@use '_colors' as colors; @import '~bulma/sass/utilities/_all'; @import '_variables.scss'; .footer { - background: $white; - border-top: 1px solid $grey-light; + background: colors.$white; + border-top: 1px solid colors.$grey-light; &__logo { display: block; @@ -15,7 +16,7 @@ &__copyright { padding-top: 1.5rem; - border-top: 1px solid $grey-light; + border-top: 1px solid colors.$grey-light; } &__link { @@ -42,7 +43,7 @@ left: 0; bottom: 0; width: 1.5rem; - border-bottom: 2px solid $redi-orange-dark; + border-bottom: 2px solid colors.$redi-orange-dark; } } } diff --git a/apps/redi-talent-pool/src/components/organisms/Footer.tsx b/apps/redi-talent-pool/src/components/organisms/Footer.tsx index e2d13802e..82d16fef4 100644 --- a/apps/redi-talent-pool/src/components/organisms/Footer.tsx +++ b/apps/redi-talent-pool/src/components/organisms/Footer.tsx @@ -1,25 +1,24 @@ -import { SocialMediaIcons } from '@talent-connect/shared-atomic-design-components' -import React from 'react' + import { Columns, Container, Element, Section } from 'react-bulma-components' import { useTranslation } from 'react-i18next' import { ReactComponent as RediSchool } from '../../assets/redi-school-logo.svg' import MicrosoftLogo from '../../assets/images/microsoft-logo.png' import DeloitteLogo from '../../assets/images/deloitte-logo.png' import './Footer.scss' +import { SocialMediaIcons } from '@talent-connect/shared-atomic-design-components'; + +type Link = { + url: string; + name: string; +} -const RediFooter = () => { +function RediFooter() { const year = new Date().getFullYear() const { t } = useTranslation() - const supportLinks: Array<{ - url: string - name: string - }> = t('footer.supportLinks', { returnObjects: true }) + const supportLinks: Link[] = t('footer.supportLinks', { returnObjects: true }) - const legalLinks: Array<{ - url: string - name: string - }> = t('footer.legalLinks', { returnObjects: true }) + const legalLinks: Link[] = t('footer.legalLinks', { returnObjects: true }) return (