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/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/OfferCard.tsx b/apps/portal/src/components/offers/profile/OfferCard.tsx index 3d03e1ef..75e5735e 100644 --- a/apps/portal/src/components/offers/profile/OfferCard.tsx +++ b/apps/portal/src/components/offers/profile/OfferCard.tsx @@ -7,7 +7,11 @@ import { import { HorizontalDivider } from '@tih/ui'; import type { OfferDisplayData } from '~/components/offers/types'; -import { JobTypeLabel } from '~/components/offers/types'; + +import { + getCompanyDisplayText, + getJobDisplayText, +} from '~/utils/offers/string'; type Props = Readonly<{ offer: OfferDisplayData; @@ -35,19 +39,18 @@ export default function OfferCard({ return (
-
- - - - - {location ? `${companyName}, ${location.cityName}` : companyName} - -
+ {(companyName || location) && ( +
+ + + + + {getCompanyDisplayText(companyName, location)} + +
+ )}
-

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

+

{getJobDisplayText(jobTitle, jobLevel, jobType)}

{!duration && receivedMonth && ( diff --git a/apps/portal/src/components/offers/profile/ProfileHeader.tsx b/apps/portal/src/components/offers/profile/ProfileHeader.tsx index 625e2be7..bb96a295 100644 --- a/apps/portal/src/components/offers/profile/ProfileHeader.tsx +++ b/apps/portal/src/components/offers/profile/ProfileHeader.tsx @@ -13,10 +13,10 @@ import { Button, Dialog, Spinner, Tabs, useToast } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import type { ProfileDetailTab } from '~/components/offers/constants'; +import { JobTypeLabel } from '~/components/offers/constants'; import { profileDetailTabs } from '~/components/offers/constants'; import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder'; import type { BackgroundDisplayData } from '~/components/offers/types'; -import { JobTypeLabel } from '~/components/offers/types'; import Tooltip from '~/components/offers/util/Tooltip'; import { getProfileEditPath } from '~/utils/offers/link'; diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx index 5ca1d91b..9022297d 100644 --- a/apps/portal/src/components/offers/table/OffersTable.tsx +++ b/apps/portal/src/components/offers/table/OffersTable.tsx @@ -2,7 +2,7 @@ import clsx from 'clsx'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import { JobType } from '@prisma/client'; -import { DropdownMenu, Spinner } from '@tih/ui'; +import { DropdownMenu, Spinner, useToast } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import OffersTablePagination from '~/components/offers/table/OffersTablePagination'; @@ -66,6 +66,7 @@ export default function OffersTable({ event?.preventDefault(); }, [yoeCategory]); + const { showToast } = useToast(); trpc.useQuery( [ 'offers.list', @@ -81,8 +82,11 @@ export default function OffersTable({ }, ], { - onError: (err) => { - alert(err); + onError: () => { + showToast({ + title: 'Error loading the page.', + variant: 'failure', + }); }, onSuccess: (response: GetOffersResponse) => { setOffers(response.data); @@ -246,7 +250,7 @@ export default function OffersTable({ {!offers || (offers.length === 0 && (
-
No data yet🥺
+
No data yet 🥺
))}
diff --git a/apps/portal/src/components/offers/table/OffersTablePagination.tsx b/apps/portal/src/components/offers/table/OffersTablePagination.tsx index 9a235901..45dd7831 100644 --- a/apps/portal/src/components/offers/table/OffersTablePagination.tsx +++ b/apps/portal/src/components/offers/table/OffersTablePagination.tsx @@ -26,7 +26,7 @@ export default function OffersTablePagination({
Showing - {` ${startNumber} - ${endNumber} `} + {` ${endNumber > 0 ? startNumber : 0} - ${endNumber} `} {`of `} diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts index 0e963a17..b38d1540 100644 --- a/apps/portal/src/components/offers/types.ts +++ b/apps/portal/src/components/offers/types.ts @@ -4,27 +4,6 @@ import type { MonthYear } from '~/components/shared/MonthYearPicker'; import type { Location } from '~/types/offers'; -export const HOME_URL = '/offers'; - -/* - * Offer Profile - */ - -export const JobTypeLabel = { - FULLTIME: 'Full-time', - INTERN: 'Internship', -}; - -export enum EducationBackgroundType { - Bachelor = 'Bachelor', - Diploma = 'Diploma', - Masters = 'Masters', - PhD = 'PhD', - Professional = 'Professional', - Secondary = 'Secondary', - SelfTaught = 'Self-taught', -} - export type OffersProfilePostData = { background: BackgroundPostData; id?: string; diff --git a/apps/portal/src/pages/offers/features.tsx b/apps/portal/src/pages/offers/features.tsx index 77ca380c..da0cc28d 100644 --- a/apps/portal/src/pages/offers/features.tsx +++ b/apps/portal/src/pages/offers/features.tsx @@ -8,12 +8,12 @@ import { UsersIcon, } from '@heroicons/react/24/outline'; +import { HOME_URL } from '~/components/offers/constants'; import offersAnalysis from '~/components/offers/features/images/offers-analysis.png'; import offersBrowse from '~/components/offers/features/images/offers-browse.png'; import offersProfile from '~/components/offers/features/images/offers-profile.png'; import LeftTextCard from '~/components/offers/features/LeftTextCard'; import RightTextCard from '~/components/offers/features/RightTextCard'; -import { HOME_URL } from '~/components/offers/types'; const features = [ { diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx index 73468325..fe39a45b 100644 --- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx @@ -6,6 +6,7 @@ import { Spinner, useToast } from '@tih/ui'; import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics'; import { ProfileDetailTab } from '~/components/offers/constants'; +import { HOME_URL } from '~/components/offers/constants'; import ProfileComments from '~/components/offers/profile/ProfileComments'; import ProfileDetails from '~/components/offers/profile/ProfileDetails'; import ProfileHeader from '~/components/offers/profile/ProfileHeader'; @@ -13,7 +14,6 @@ import type { BackgroundDisplayData, OfferDisplayData, } from '~/components/offers/types'; -import { HOME_URL } from '~/components/offers/types'; import type { JobTitleType } from '~/components/shared/JobTitles'; import { getLabelForJobTitleType } from '~/components/shared/JobTitles'; diff --git a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx index 35c72796..f507283d 100644 --- a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx +++ b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx @@ -1,11 +1,12 @@ +import Error from 'next/error'; import { useRouter } from 'next/router'; import { useEffect, useRef, useState } from 'react'; import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid'; import { EyeIcon } from '@heroicons/react/24/outline'; import { Button, Spinner } from '@tih/ui'; -import type { BreadcrumbStep } from '~/components/offers/Breadcrumb'; -import { Breadcrumbs } from '~/components/offers/Breadcrumb'; +import type { BreadcrumbStep } from '~/components/offers/Breadcrumbs'; +import { Breadcrumbs } from '~/components/offers/Breadcrumbs'; import OffersProfileSave from '~/components/offers/offersSubmission/OffersProfileSave'; import OffersSubmissionAnalysis from '~/components/offers/offersSubmission/OffersSubmissionAnalysis'; @@ -21,12 +22,21 @@ export default function OffersSubmissionResult() { token = token as string; const [step, setStep] = useState(0); const [analysis, setAnalysis] = useState(null); + const [isValidToken, setIsValidToken] = useState(false); const pageRef = useRef(null); const scrollToTop = () => pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 }); - // TODO: Check if the token is valid before showing this page + const checkToken = trpc.useQuery( + ['offers.profile.isValidToken', { profileId: offerProfileId, token }], + { + onSuccess(data) { + setIsValidToken(data); + }, + }, + ); + const getAnalysis = trpc.useQuery( ['offers.analysis.get', { profileId: offerProfileId }], { @@ -69,7 +79,7 @@ export default function OffersSubmissionResult() { return ( <> - {getAnalysis.isLoading && ( + {(checkToken.isLoading || getAnalysis.isLoading) && (
@@ -77,7 +87,13 @@ export default function OffersSubmissionResult() {
)} - {!getAnalysis.isLoading && ( + {checkToken.isSuccess && !isValidToken && ( + + )} + {getAnalysis.isSuccess && (
diff --git a/apps/portal/src/utils/offers/currency/CurrencySelector.tsx b/apps/portal/src/utils/offers/currency/CurrencySelector.tsx index 2ba883b4..2b339933 100644 --- a/apps/portal/src/utils/offers/currency/CurrencySelector.tsx +++ b/apps/portal/src/utils/offers/currency/CurrencySelector.tsx @@ -20,7 +20,7 @@ export default function CurrencySelector({