diff --git a/apps/portal/src/components/offers/Breadcrumb.tsx b/apps/portal/src/components/offers/Breadcrumbs.tsx similarity index 100% rename from apps/portal/src/components/offers/Breadcrumb.tsx rename to apps/portal/src/components/offers/Breadcrumbs.tsx diff --git a/apps/portal/src/components/offers/EducationFields.ts b/apps/portal/src/components/offers/EducationFields.ts new file mode 100644 index 00000000..6818b9ab --- /dev/null +++ b/apps/portal/src/components/offers/EducationFields.ts @@ -0,0 +1,16 @@ +import { emptyOption } from './constants'; + +export const EducationFieldLabels = [ + 'Business Analytics', + 'Computer Science', + 'Data Science and Analytics', + 'Information Security', + 'Information Systems', +]; + +export const EducationFieldOptions = [emptyOption].concat( + EducationFieldLabels.map((label) => ({ + label, + value: label.replace(/\s+/g, '-').toLowerCase(), + })), +); diff --git a/apps/portal/src/components/offers/EducationLevels.ts b/apps/portal/src/components/offers/EducationLevels.ts new file mode 100644 index 00000000..176b2519 --- /dev/null +++ b/apps/portal/src/components/offers/EducationLevels.ts @@ -0,0 +1,18 @@ +import { emptyOption } from './constants'; + +export const EducationLevelLabels = [ + 'Bachelor', + 'Diploma', + 'Masters', + 'PhD', + 'Professional', + 'Secondary', + 'Self-taught', +]; + +export const EducationLevelOptions = [emptyOption].concat( + EducationLevelLabels.map((label) => ({ + label, + value: label.replace(/\s+/g, '-').toLowerCase(), + })), +); diff --git a/apps/portal/src/components/offers/InternshipCycles.ts b/apps/portal/src/components/offers/InternshipCycles.ts new file mode 100644 index 00000000..1f90539f --- /dev/null +++ b/apps/portal/src/components/offers/InternshipCycles.ts @@ -0,0 +1,18 @@ +import { emptyOption } from './constants'; + +export const InternshipCycleLabels = [ + 'Spring', + 'Summer', + 'Fall', + 'Winter', + 'Half year', + 'Full year', + 'Others', +]; + +export const InternshipCycleOptions = [emptyOption].concat( + InternshipCycleLabels.map((label) => ({ + label, + value: label.replace(/\s+/g, '-').toLowerCase(), + })), +); diff --git a/apps/portal/src/components/offers/JobTypeTabs.tsx b/apps/portal/src/components/offers/JobTypeTabs.tsx index 8ea87bc6..22d70ea7 100644 --- a/apps/portal/src/components/offers/JobTypeTabs.tsx +++ b/apps/portal/src/components/offers/JobTypeTabs.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import { JobType } from '@prisma/client'; -import { JobTypeLabel } from './types'; +import { JobTypeLabel } from '~/components/offers/constants'; type Props = Readonly<{ onChange: (jobType: JobType) => void; diff --git a/apps/portal/src/components/offers/Years.ts b/apps/portal/src/components/offers/Years.ts new file mode 100644 index 00000000..da9ab8e3 --- /dev/null +++ b/apps/portal/src/components/offers/Years.ts @@ -0,0 +1,8 @@ +const NUM_YEARS = 5; +export const FutureYearsOptions = Array.from({ length: NUM_YEARS }, (_, i) => { + const year = new Date().getFullYear() + i; + return { + label: String(year), + value: year, + }; +}); diff --git a/apps/portal/src/components/offers/constants.ts b/apps/portal/src/components/offers/constants.ts index d49dca1a..e84dfd4e 100644 --- a/apps/portal/src/components/offers/constants.ts +++ b/apps/portal/src/components/offers/constants.ts @@ -1,78 +1,14 @@ -import { EducationBackgroundType } from './types'; +export const HOME_URL = '/offers'; -export const emptyOption = '----'; +export const JobTypeLabel = { + FULLTIME: 'Full-time', + INTERN: 'Internship', +}; -export const internshipCycleOptions = [ - { - label: 'Summer', - value: 'Summer', - }, - { - label: 'Winter', - value: 'Winter', - }, - { - label: 'Spring', - value: 'Spring', - }, - { - label: 'Fall', - value: 'Fall', - }, - { - label: 'Full year', - value: 'Full year', - }, -]; - -export const yearOptions = [ - { - label: '2021', - value: 2021, - }, - { - label: '2022', - value: 2022, - }, - { - label: '2023', - value: 2023, - }, - { - label: '2024', - value: 2024, - }, -]; - -export const educationLevelOptions = Object.entries( - EducationBackgroundType, -).map(([, value]) => ({ - label: value, - value, -})); - -export const educationFieldOptions = [ - { - label: 'Computer Science', - value: 'Computer Science', - }, - { - label: 'Information Security', - value: 'Information Security', - }, - { - label: 'Information Systems', - value: 'Information Systems', - }, - { - label: 'Business Analytics', - value: 'Business Analytics', - }, - { - label: 'Data Science and Analytics', - value: 'Data Science and Analytics', - }, -]; +export const emptyOption = { + label: '', + value: '', +}; export enum FieldError { NON_NEGATIVE_NUMBER = 'Please fill in a non-negative number in this field.', diff --git a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx index 722263b6..dda493ec 100644 --- a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx +++ b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx @@ -37,28 +37,28 @@ export default function DashboardProfileCard({
{company?.name && ( -
+
)} {location && ( -
+
)} {level && ( -
+
diff --git a/apps/portal/src/components/offers/features/LeftTextCard.tsx b/apps/portal/src/components/offers/features/LeftTextCard.tsx index b4fac765..867b24b7 100644 --- a/apps/portal/src/components/offers/features/LeftTextCard.tsx +++ b/apps/portal/src/components/offers/features/LeftTextCard.tsx @@ -2,7 +2,7 @@ import type { StaticImageData } from 'next/image'; import Image from 'next/image'; import type { ReactNode } from 'react'; -import { HOME_URL } from '~/components/offers/types'; +import { HOME_URL } from '../constants'; type LeftTextCardProps = Readonly<{ description: string; diff --git a/apps/portal/src/components/offers/features/RightTextCard.tsx b/apps/portal/src/components/offers/features/RightTextCard.tsx index 9ca0f949..028dc57b 100644 --- a/apps/portal/src/components/offers/features/RightTextCard.tsx +++ b/apps/portal/src/components/offers/features/RightTextCard.tsx @@ -2,7 +2,7 @@ import type { StaticImageData } from 'next/image'; import Image from 'next/image'; import type { ReactNode } from 'react'; -import { HOME_URL } from '~/components/offers/types'; +import { HOME_URL } from '../constants'; type RightTextCarddProps = Readonly<{ description: string; diff --git a/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx b/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx index bdf9a1b3..4c7ecca3 100644 --- a/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx +++ b/apps/portal/src/components/offers/offerAnalysis/OfferProfileCard.tsx @@ -9,10 +9,11 @@ import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; import { HorizontalDivider } from '~/../../../packages/ui/dist'; import { convertMoneyToString } from '~/utils/offers/currency'; +import { getCompanyDisplayText } from '~/utils/offers/string'; import { formatDate } from '~/utils/offers/time'; +import { JobTypeLabel } from '../constants'; import ProfilePhotoHolder from '../profile/ProfilePhotoHolder'; -import { JobTypeLabel } from '../types'; import type { AnalysisOffer } from '~/types/offers'; @@ -69,7 +70,7 @@ export default function OfferProfileCard({ {getLabelForJobTitleType(title as JobTitleType)}{' '} {`(${JobTypeLabel[jobType]})`}

-

{`Company: ${company.name}, ${location}`}

+

{`Company: ${getCompanyDisplayText(company.name, location)}`}

{level &&

Level: {level}

}
diff --git a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx index b72641d8..d494912e 100644 --- a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx +++ b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx @@ -93,7 +93,6 @@ export default function OffersProfileSave({
(null); const scrollToTop = () => @@ -132,13 +133,7 @@ export default function OffersSubmissionForm({ }, ); - const steps = [ - , - , - ]; + const steps = [, ]; const breadcrumbSteps: Array = [ { @@ -157,14 +152,14 @@ export default function OffersSubmissionForm({ }, ]; - const goToNextStep = async (currStep: number) => { - if (currStep === 0) { + const setStepWithValidation = async (nextStep: number) => { + if (nextStep === 1) { const result = await trigger('offers'); if (!result) { return; } } - setStep(step + 1); + setStep(nextStep); }; const mutationpath = @@ -175,10 +170,24 @@ export default function OffersSubmissionForm({ const createOrUpdateMutation = trpc.useMutation([mutationpath], { onError(error) { console.error(error.message); + showToast({ + title: + editProfileId && editToken + ? 'Error updating offer profile.' + : 'Error creating offer profile', + variant: 'failure', + }); }, onSuccess(data) { setParams({ profileId: data.id, token: data.token }); setIsSubmitted(true); + showToast({ + title: + editProfileId && editToken + ? 'Offer profile updated successfully!' + : 'Offer profile created successfully!', + variant: 'success', + }); }, }); @@ -270,7 +279,7 @@ export default function OffersSubmissionForm({
@@ -288,7 +297,7 @@ export default function OffersSubmissionForm({ label="Next" variant="primary" onClick={() => { - goToNextStep(step); + setStepWithValidation(step + 1); gaEvent({ action: 'offers.profile_submission_navigate_next', category: 'submission', diff --git a/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx index bfe5ee5f..d951f088 100644 --- a/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx @@ -2,12 +2,7 @@ import { useFormContext, useWatch } from 'react-hook-form'; import { JobType } from '@prisma/client'; import { Collapsible, RadioList } from '@tih/ui'; -import { - educationFieldOptions, - educationLevelOptions, - emptyOption, - FieldError, -} from '~/components/offers/constants'; +import { FieldError } from '~/components/offers/constants'; import type { BackgroundPostData } from '~/components/offers/types'; import CitiesTypeahead from '~/components/shared/CitiesTypeahead'; import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead'; @@ -20,6 +15,8 @@ import { CURRENCY_OPTIONS, } from '~/utils/offers/currency/CurrencyEnum'; +import { EducationFieldOptions } from '../../EducationFields'; +import { EducationLevelOptions } from '../../EducationLevels'; import FormRadioList from '../../forms/FormRadioList'; import FormSection from '../../forms/FormSection'; import FormSelect from '../../forms/FormSelect'; @@ -134,6 +131,9 @@ function FullTimeJobFields() { if (option) { setValue('background.experiences.0.companyId', option.value); setValue('background.experiences.0.companyName', option.label); + } else { + setValue('background.experiences.0.companyId', ''); + setValue('background.experiences.0.companyName', ''); } }} /> @@ -343,15 +343,13 @@ function EducationSection() {
diff --git a/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx index 7c9e52e6..1fc38fe2 100644 --- a/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx +++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/OfferDetailsForm.tsx @@ -22,20 +22,16 @@ import { defaultFullTimeOfferValues, defaultInternshipOfferValues, } from '../OffersSubmissionForm'; -import { - emptyOption, - FieldError, - internshipCycleOptions, - yearOptions, -} from '../../constants'; +import { FieldError, JobTypeLabel } from '../../constants'; import FormMonthYearPicker from '../../forms/FormMonthYearPicker'; import FormSection from '../../forms/FormSection'; import FormSelect from '../../forms/FormSelect'; import FormTextArea from '../../forms/FormTextArea'; import FormTextInput from '../../forms/FormTextInput'; +import { InternshipCycleOptions } from '../../InternshipCycles'; import JobTypeTabs from '../../JobTypeTabs'; import type { OfferFormData } from '../../types'; -import { JobTypeLabel } from '../../types'; +import { FutureYearsOptions } from '../../Years'; import { Currency, CURRENCY_OPTIONS, @@ -384,8 +380,7 @@ function InternshipOfferDetailsForm({ display="block" errorMessage={offerFields?.offersIntern?.internshipCycle?.message} label="Internship Cycle" - options={internshipCycleOptions} - placeholder={emptyOption} + options={InternshipCycleOptions} required={true} {...register(`offers.${index}.offersIntern.internshipCycle`, { required: FieldError.REQUIRED, @@ -395,8 +390,7 @@ function InternshipOfferDetailsForm({ display="block" errorMessage={offerFields?.offersIntern?.startYear?.message} label="Internship Year" - options={yearOptions} - placeholder={emptyOption} + options={FutureYearsOptions} required={true} {...register(`offers.${index}.offersIntern.startYear`, { required: FieldError.REQUIRED, @@ -522,14 +516,11 @@ function OfferDetailsFormArray({ ); } -type OfferDetailsFormProps = Readonly<{ - defaultJobType?: JobType; -}>; - -export default function OfferDetailsForm({ - defaultJobType = JobType.FULLTIME, -}: OfferDetailsFormProps) { - const [jobType, setJobType] = useState(defaultJobType); +export default function OfferDetailsForm() { + const watchJobType = useWatch({ + name: `offers.0.jobType`, + }); + const [jobType, setJobType] = useState(watchJobType as JobType); const [isDialogOpen, setDialogOpen] = useState(false); const { control } = useFormContext(); const fieldArrayValues = useFieldArray({ control, name: 'offers' }); @@ -576,8 +567,8 @@ export default function OfferDetailsForm({ label="Switch" variant="primary" onClick={() => { - toggleJobType(); setDialogOpen(false); + toggleJobType(); }} /> } diff --git a/apps/portal/src/components/offers/profile/EducationCard.tsx b/apps/portal/src/components/offers/profile/EducationCard.tsx index c885ef5a..9549e423 100644 --- a/apps/portal/src/components/offers/profile/EducationCard.tsx +++ b/apps/portal/src/components/offers/profile/EducationCard.tsx @@ -13,12 +13,12 @@ export default function EducationCard({ education: { type, field, startDate, endDate, school }, }: Props) { return ( -
-
-
-
+
+
+
+
- + {field ? `${type ?? 'N/A'}, ${field}` : type ?? `N/A`}
diff --git a/apps/portal/src/components/offers/profile/OfferCard.tsx b/apps/portal/src/components/offers/profile/OfferCard.tsx index 3d03e1ef..68f1571a 100644 --- a/apps/portal/src/components/offers/profile/OfferCard.tsx +++ b/apps/portal/src/components/offers/profile/OfferCard.tsx @@ -1,13 +1,13 @@ import { - BuildingOffice2Icon, - ChatBubbleBottomCenterTextIcon, - CurrencyDollarIcon, - ScaleIcon, -} from '@heroicons/react/24/outline'; -import { HorizontalDivider } from '@tih/ui'; + ArrowTrendingUpIcon, + BuildingOfficeIcon, + MapPinIcon, +} from '@heroicons/react/20/solid'; +import { JobTypeLabel } from '~/components/offers/constants'; import type { OfferDisplayData } from '~/components/offers/types'; -import { JobTypeLabel } from '~/components/offers/types'; + +import { getLocationDisplayText } from '~/utils/offers/string'; type Props = Readonly<{ offer: OfferDisplayData; @@ -33,33 +33,55 @@ export default function OfferCard({ }: Props) { function UpperSection() { return ( -
-
-
- - - - - {location ? `${companyName}, ${location.cityName}` : companyName} - +
+
+
+

+ {jobTitle} {jobType && <>({JobTypeLabel[jobType]})} +

+
+ {companyName && ( +
+
+ )} + {location && ( +
+
+ )} + {jobLevel && ( +
+
+ )} +
-
-

- {jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}{' '} - {jobType && `(${JobTypeLabel[jobType]})`} -

+
+ {!duration && receivedMonth && ( +
+

{receivedMonth}

+
+ )} + {duration && ( +
+

{`${duration} months`}

+
+ )}
- {!duration && receivedMonth && ( -
-

{receivedMonth}

-
- )} - {duration && ( -
-

{`${duration} months`}

-
- )}
); } @@ -75,60 +97,69 @@ export default function OfferCard({ } return ( - <> - -
-
- {(totalCompensation || monthlySalary) && ( -
- - - - -

- {totalCompensation && `TC: ${totalCompensation}`} - {monthlySalary && `Monthly Salary: ${monthlySalary}`} -

-
-
- )} - {(base || stocks || bonus) && totalCompensation && ( -
-

- Base / year: {base ?? 'N/A'} ⋅ Stocks / year:{' '} - {stocks ?? 'N/A'} ⋅ Bonus / year: {bonus ?? 'N/A'} -

-
- )} -
+
+
+ {totalCompensation && ( +
+
+ Total Compensation +
+
+ {totalCompensation} +
+
+ )} + {monthlySalary && ( +
+
+ Monthly Salary +
+
{monthlySalary}
+
+ )} + {base && ( +
+
+ Base Salary +
+
{base}
+
+ )} + {stocks && ( +
+
Stocks
+
{stocks}
+
+ )} + {bonus && ( +
+
Bonus
+
{bonus}
+
+ )} {negotiationStrategy && ( -
-
- - - - - "{negotiationStrategy}" - -
+
+
+ Negotiation Strategy +
+
+ {negotiationStrategy} +
)} {otherComment && ( -
-
- - - - "{otherComment}" -
+
+
Others
+
{otherComment}
)} -
- +
+
); } + return ( -
+
diff --git a/apps/portal/src/components/offers/profile/ProfileComments.tsx b/apps/portal/src/components/offers/profile/ProfileComments.tsx index dc0ec114..34ac8b67 100644 --- a/apps/portal/src/components/offers/profile/ProfileComments.tsx +++ b/apps/portal/src/components/offers/profile/ProfileComments.tsx @@ -110,10 +110,10 @@ export default function ProfileComments({ ); } return ( -
-
+
+
-
+
{isEditable && ( @@ -169,57 +169,62 @@ export default function ProfileComments({
-

Discussions

- {isEditable || session?.user?.name ? ( -
-