diff --git a/apps/portal/prisma/migrations/20221104042559_add_reference_to_analysed_offer_in_analysis_unit/migration.sql b/apps/portal/prisma/migrations/20221104042559_add_reference_to_analysed_offer_in_analysis_unit/migration.sql
new file mode 100644
index 00000000..3a29e11f
--- /dev/null
+++ b/apps/portal/prisma/migrations/20221104042559_add_reference_to_analysed_offer_in_analysis_unit/migration.sql
@@ -0,0 +1,16 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `companyId` on the `OffersAnalysisUnit` table. All the data in the column will be lost.
+ - Added the required column `analysedOfferId` to the `OffersAnalysisUnit` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- DropForeignKey
+ALTER TABLE "OffersAnalysisUnit" DROP CONSTRAINT "OffersAnalysisUnit_companyId_fkey";
+
+-- AlterTable
+ALTER TABLE "OffersAnalysisUnit" DROP COLUMN "companyId",
+ADD COLUMN "analysedOfferId" TEXT NOT NULL;
+
+-- AddForeignKey
+ALTER TABLE "OffersAnalysisUnit" ADD CONSTRAINT "OffersAnalysisUnit_analysedOfferId_fkey" FOREIGN KEY ("analysedOfferId") REFERENCES "OffersOffer"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/apps/portal/prisma/schema.prisma b/apps/portal/prisma/schema.prisma
index e39941f2..cbf65808 100644
--- a/apps/portal/prisma/schema.prisma
+++ b/apps/portal/prisma/schema.prisma
@@ -104,7 +104,6 @@ model Company {
questionsQuestionEncounter QuestionsQuestionEncounter[]
OffersExperience OffersExperience[]
OffersOffer OffersOffer[]
- OffersAnalysisUnit OffersAnalysisUnit[]
}
model Country {
@@ -368,6 +367,7 @@ model OffersOffer {
offersAnalysis OffersAnalysis? @relation("HighestOverallOffer")
offersAnalysisUnit OffersAnalysisUnit[]
+ OffersAnalysisUnit OffersAnalysisUnit[] @relation("Analysed Offer")
}
model OffersIntern {
@@ -419,8 +419,8 @@ model OffersAnalysis {
model OffersAnalysisUnit {
id String @id @default(cuid())
- company Company @relation(fields: [companyId], references: [id])
- companyId String
+ analysedOffer OffersOffer @relation("Analysed Offer", fields: [analysedOfferId], references: [id])
+ analysedOfferId String
percentile Float
noOfSimilarOffers Int
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..3b013c2c 100644
--- a/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx
+++ b/apps/portal/src/components/offers/dashboard/DashboardOfferCard.tsx
@@ -5,6 +5,7 @@ import {
} from '@heroicons/react/20/solid';
import { JobType } from '@prisma/client';
+import { JobTypeLabel } from '~/components/offers/constants';
import type { JobTitleType } from '~/components/shared/JobTitles';
import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
@@ -33,32 +34,33 @@ export default function DashboardProfileCard({
- {getLabelForJobTitleType(title as JobTitleType)}
+ {getLabelForJobTitleType(title as JobTitleType)}{' '}
+ {jobType && <>({JobTypeLabel[jobType]})>}
{company?.name && (
-
+
{company.name}
)}
{location && (
-
+
{location.cityName}
)}
{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/OfferAnalysis.tsx b/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx
index e7e6199b..d229eb52 100644
--- a/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx
+++ b/apps/portal/src/components/offers/offerAnalysis/OfferAnalysis.tsx
@@ -109,13 +109,13 @@ export default function OfferAnalysis({
return (
- {isError && (
+ {isError ? (
An error occurred while generating profile analysis.
- )}
- {isLoading &&
}
- {!isError && !isLoading && (
+ ) : isLoading ? (
+
+ ) : (
-
+
);
}
diff --git a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx
index b72641d8..513d0de7 100644
--- a/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx
+++ b/apps/portal/src/components/offers/offersSubmission/OffersProfileSave.tsx
@@ -1,5 +1,5 @@
import { signIn, useSession } from 'next-auth/react';
-import { useState } from 'react';
+import type { UseQueryResult } from 'react-query';
import { DocumentDuplicateIcon } from '@heroicons/react/20/solid';
import { BookmarkIcon as BookmarkOutlineIcon } from '@heroicons/react/24/outline';
import { BookmarkIcon as BookmarkSolidIcon } from '@heroicons/react/24/solid';
@@ -11,6 +11,7 @@ import { copyProfileLink, getProfileLink } from '~/utils/offers/link';
import { trpc } from '~/utils/trpc';
type OfferProfileSaveProps = Readonly<{
+ isSavedQuery: UseQueryResult;
profileId: string;
token?: string;
}>;
@@ -18,10 +19,10 @@ type OfferProfileSaveProps = Readonly<{
export default function OffersProfileSave({
profileId,
token,
+ isSavedQuery: { data: isSaved, isLoading },
}: OfferProfileSaveProps) {
const { showToast } = useToast();
const { event: gaEvent } = useGoogleAnalytics();
- const [isSaved, setSaved] = useState(false);
const { data: session, status } = useSession();
const saveMutation = trpc.useMutation(
@@ -47,15 +48,6 @@ export default function OffersProfileSave({
},
);
- const isSavedQuery = trpc.useQuery(
- [`offers.profile.isSaved`, { profileId, userId: session?.user?.id }],
- {
- onSuccess: (res) => {
- setSaved(res);
- },
- },
- );
-
const trpcContext = trpc.useContext();
const handleSave = () => {
if (status === 'unauthenticated') {
@@ -93,7 +85,6 @@ export default function OffersProfileSave({
)}
diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx
index 06415aad..a03782dd 100644
--- a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx
+++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx
@@ -4,11 +4,11 @@ import type { SubmitHandler } from 'react-hook-form';
import { FormProvider, useForm } from 'react-hook-form';
import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/20/solid';
import { JobType } from '@prisma/client';
-import { Button } from '@tih/ui';
+import { Button, Spinner, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
-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 BackgroundForm from '~/components/offers/offersSubmission/submissionForm/BackgroundForm';
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
import type {
@@ -102,8 +102,9 @@ export default function OffersSubmissionForm({
token: editToken,
});
const [isSubmitted, setIsSubmitted] = useState(false);
- const { event: gaEvent } = useGoogleAnalytics();
+ const { event: gaEvent } = useGoogleAnalytics();
+ const { showToast } = useToast();
const router = useRouter();
const pageRef = useRef(null);
const scrollToTop = () =>
@@ -115,7 +116,7 @@ export default function OffersSubmissionForm({
const {
handleSubmit,
trigger,
- formState: { isSubmitting, isSubmitSuccessful },
+ formState: { isSubmitting },
} = formMethods;
const generateAnalysisMutation = trpc.useMutation(
@@ -123,6 +124,10 @@ export default function OffersSubmissionForm({
{
onError(error) {
console.error(error.message);
+ showToast({
+ title: 'Error generating analysis.',
+ variant: 'failure',
+ });
},
onSuccess() {
router.push(
@@ -132,13 +137,7 @@ export default function OffersSubmissionForm({
},
);
- const steps = [
- ,
- ,
- ];
+ const steps = [, ];
const breadcrumbSteps: Array = [
{
@@ -157,14 +156,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,16 +174,30 @@ 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',
+ });
},
});
const onSubmit: SubmitHandler = async (data) => {
const result = await trigger();
- if (!result || isSubmitting || isSubmitSuccessful) {
+ if (!result || isSubmitting || createOrUpdateMutation.isLoading) {
return;
}
@@ -263,14 +276,16 @@ export default function OffersSubmissionForm({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- return (
+ return generateAnalysisMutation.isLoading ? (
+
+ ) : (
@@ -288,7 +303,7 @@ export default function OffersSubmissionForm({
label="Next"
variant="primary"
onClick={() => {
- goToNextStep(step);
+ setStepWithValidation(step + 1);
gaEvent({
action: 'offers.profile_submission_navigate_next',
category: 'submission',
@@ -315,9 +330,16 @@ export default function OffersSubmissionForm({
}}
/>
@@ -279,23 +279,34 @@ function InternshipJobFields() {
})}
/>
- {
- if (option) {
- setValue('background.experiences.0.cityId', option.value);
- setValue('background.experiences.0.cityName', option.label);
- } else {
- setValue('background.experiences.0.cityId', '');
- setValue('background.experiences.0.cityName', '');
- }
- }}
- />
+
+ {
+ if (option) {
+ setValue('background.experiences.0.cityId', option.value);
+ setValue('background.experiences.0.cityName', option.label);
+ } else {
+ setValue('background.experiences.0.cityId', '');
+ setValue('background.experiences.0.cityName', '');
+ }
+ }}
+ />
+
+
>
);
@@ -343,15 +354,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..42212fce 100644
--- a/apps/portal/src/components/offers/profile/OfferCard.tsx
+++ b/apps/portal/src/components/offers/profile/OfferCard.tsx
@@ -1,13 +1,15 @@
import {
- BuildingOffice2Icon,
- ChatBubbleBottomCenterTextIcon,
- CurrencyDollarIcon,
- ScaleIcon,
-} from '@heroicons/react/24/outline';
-import { HorizontalDivider } from '@tih/ui';
+ ArrowTrendingUpIcon,
+ BuildingOfficeIcon,
+ MapPinIcon,
+} from '@heroicons/react/20/solid';
+import { JobType } from '@prisma/client';
+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';
+import { getDurationDisplayText } from '~/utils/offers/time';
type Props = Readonly<{
offer: OfferDisplayData;
@@ -33,33 +35,55 @@ export default function OfferCard({
}: Props) {
function UpperSection() {
return (
-
-
-
-
-
-
-
- {location ? `${companyName}, ${location.cityName}` : companyName}
-
+
+
+
+
+ {jobTitle} {jobType && <>({JobTypeLabel[jobType]})>}
+
+
+ {companyName && (
+
+
+ {companyName}
+
+ )}
+ {location && (
+
+
+ {getLocationDisplayText(location)}
+
+ )}
+ {jobLevel && (
+
+ )}
+
-
-
- {jobLevel ? `${jobTitle}, ${jobLevel}` : jobTitle}{' '}
- {jobType && `(${JobTypeLabel[jobType]})`}
-
+
+ {!duration && receivedMonth && (
+
+ )}
+ {!!duration && (
+
+
{getDurationDisplayText(duration)}
+
+ )}
- {!duration && receivedMonth && (
-
- )}
- {duration && (
-
-
{`${duration} months`}
-
- )}
);
}
@@ -75,60 +99,72 @@ 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'}
-
-
- )}
-
+
+
+ {jobType === JobType.FULLTIME
+ ? 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 5654ed5e..0eeaa1a3 100644
--- a/apps/portal/src/components/offers/profile/ProfileComments.tsx
+++ b/apps/portal/src/components/offers/profile/ProfileComments.tsx
@@ -1,13 +1,7 @@
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import { ClipboardDocumentIcon, ShareIcon } from '@heroicons/react/24/outline';
-import {
- Button,
- HorizontalDivider,
- Spinner,
- TextArea,
- useToast,
-} from '@tih/ui';
+import { Button, Spinner, TextArea, useToast } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import ExpandableCommentCard from '~/components/offers/profile/comments/ExpandableCommentCard';
@@ -110,10 +104,10 @@ export default function ProfileComments({
);
}
return (
-
-
+
+
-
+
{isEditable && (
@@ -169,57 +163,60 @@ export default function ProfileComments({
-
Discussions
- {isEditable || session?.user?.name ? (
-
);
}
diff --git a/apps/portal/src/components/offers/profile/ProfileDetails.tsx b/apps/portal/src/components/offers/profile/ProfileDetails.tsx
index 3f61060b..1d606c03 100644
--- a/apps/portal/src/components/offers/profile/ProfileDetails.tsx
+++ b/apps/portal/src/components/offers/profile/ProfileDetails.tsx
@@ -25,19 +25,19 @@ type ProfileOffersProps = Readonly<{
}>;
function ProfileOffers({ offers }: ProfileOffersProps) {
- if (offers.length !== 0) {
+ if (offers.length === 0) {
return (
- <>
- {offers.map((offer) => (
-
- ))}
- >
+
+
No offers are attached.
+
);
}
+
return (
-
-
-
No offer is attached.
+
+ {offers.map((offer) => (
+
+ ))}
);
}
@@ -49,33 +49,37 @@ type ProfileBackgroundProps = Readonly<{
function ProfileBackground({ background }: ProfileBackgroundProps) {
if (!background?.experiences?.length && !background?.educations?.length) {
return (
-
+
No background information available.
);
}
return (
- <>
+
{background?.experiences?.length > 0 && (
- <>
-
-
-
Work Experience
+
+
+
+
+ Work Experience
+
- >
+
)}
{background?.educations?.length > 0 && (
- <>
-
);
}
@@ -114,7 +118,7 @@ function ProfileAnalysis({
}
return (
-
+
{!analysis ? (
No analysis available.
) : (
@@ -165,12 +169,15 @@ export default function ProfileDetails({
);
}
+
if (selectedTab === ProfileDetailTab.OFFERS) {
return
;
}
+
if (selectedTab === ProfileDetailTab.BACKGROUND) {
return
;
}
+
if (selectedTab === ProfileDetailTab.ANALYSIS) {
return (
-
-
-
+
+
+
+
-
+
{profileName ?? 'anonymous'}
{(experiences[0]?.companyName ||
experiences[0]?.jobLevel ||
experiences[0]?.jobTitle) && (
-
+
@@ -262,7 +262,7 @@ export default function ProfileHeader({
)}
-
+
YOE:
@@ -286,7 +286,7 @@ export default function ProfileHeader({
)}
-
+
;
export default function ProfilePhotoHolder({
size = 'lg',
}: ProfilePhotoHolderProps) {
- const sizeMap = { lg: '16', sm: '12' };
+ const sizeMap = { lg: '16', sm: '12', xs: '10' };
return (
diff --git a/apps/portal/src/components/offers/profile/comments/CommentCard.tsx b/apps/portal/src/components/offers/profile/comments/CommentCard.tsx
index c94d7b5a..b958d329 100644
--- a/apps/portal/src/components/offers/profile/comments/CommentCard.tsx
+++ b/apps/portal/src/components/offers/profile/comments/CommentCard.tsx
@@ -1,14 +1,11 @@
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
-import {
- ChatBubbleBottomCenterIcon,
- TrashIcon,
-} from '@heroicons/react/24/outline';
-import { Button, Dialog, HorizontalDivider, TextArea, useToast } from '@tih/ui';
+import { Button, Dialog, TextArea, useToast } from '@tih/ui';
-import { timeSinceNow } from '~/utils/offers/time';
+import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
-import { trpc } from '../../../../utils/trpc';
+import { timeSinceNow } from '~/utils/offers/time';
+import { trpc } from '~/utils/trpc';
import type { Reply } from '~/types/offers';
@@ -125,83 +122,105 @@ export default function CommentCard({
}
return (
- <>
-
-
-
+
+
+ {user?.image ? (
+

+ ) : (
+
+ )}
+
+
+
+
{user?.name ?? 'unknown user'}
-
-
{message}
-
-
{`${timeSinceNow(
- createdAt,
- )} ago`}
- {replyLength > 0 && (
-
+
+
+
+
+ {timeSinceNow(createdAt)} ago
+ {' '}
+ {replyLength > 0 && (
+ <>
+ ·{' '}
+
- )}
- {!disableReply && (
-
-
- )}
- {deletable && (
- <>
-
- {!disableReply && isReplying && (
-
+
+ >
+ )}
+ {!disableReply && (
+ <>
+
·{' '}
+
setIsReplying(!isReplying)}>
+ Reply
+
+ >
+ )}
+ {deletable && (
+ <>
+
·{' '}
+
setIsDialogOpen(true)}>
+ {deleteCommentMutation.isLoading ? 'Deleting...' : 'Delete'}
+
+ {isDialogOpen && (
+
+ )}
+ >
+ )}
+
+ {!disableReply && isReplying && (
+
+
-
- )}
-
+
+
+ )}
-
- >
+
);
}
diff --git a/apps/portal/src/components/offers/profile/comments/ExpandableCommentCard.tsx b/apps/portal/src/components/offers/profile/comments/ExpandableCommentCard.tsx
index 22736196..3f160016 100644
--- a/apps/portal/src/components/offers/profile/comments/ExpandableCommentCard.tsx
+++ b/apps/portal/src/components/offers/profile/comments/ExpandableCommentCard.tsx
@@ -26,18 +26,20 @@ export default function ExpandableCommentCard({
replyLength={comment.replies?.length ?? 0}
token={token}
/>
- {comment.replies && (
-
- {isExpanded &&
- comment.replies.map((reply) => (
-
+ {comment.replies && comment.replies.length > 0 && isExpanded && (
+
+
+ {comment.replies.map((reply) => (
+ -
+
+
))}
+
)}
diff --git a/apps/portal/src/components/offers/table/OffersTable.tsx b/apps/portal/src/components/offers/table/OffersTable.tsx
index 5ca1d91b..209a9ec8 100644
--- a/apps/portal/src/components/offers/table/OffersTable.tsx
+++ b/apps/portal/src/components/offers/table/OffersTable.tsx
@@ -1,14 +1,14 @@
import clsx from 'clsx';
import { useRouter } from 'next/router';
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, 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';
+import type { OfferTableSortByType } from '~/components/offers/table/types';
import {
OfferTableFilterOptions,
- OfferTableSortBy,
OfferTableYoeOptions,
YOE_CATEGORY,
YOE_CATEGORY_PARAM,
@@ -16,6 +16,7 @@ import {
import { Currency } from '~/utils/offers/currency/CurrencyEnum';
import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
+import { useSearchParamSingle } from '~/utils/offers/useSearchParam';
import { trpc } from '~/utils/trpc';
import OffersRow from './OffersRow';
@@ -25,16 +26,17 @@ import type { DashboardOffer, GetOffersResponse, Paging } from '~/types/offers';
const NUMBER_OF_OFFERS_IN_PAGE = 10;
export type OffersTableProps = Readonly<{
companyFilter: string;
+ companyName?: string;
countryFilter: string;
jobTitleFilter: string;
}>;
export default function OffersTable({
countryFilter,
+ companyName,
companyFilter,
jobTitleFilter,
}: OffersTableProps) {
const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location
- const [selectedYoe, setSelectedYoe] = useState('');
const [jobType, setJobType] = useState
(JobType.FULLTIME);
const [pagination, setPagination] = useState({
currentPage: 0,
@@ -42,30 +44,64 @@ export default function OffersTable({
numOfPages: 0,
totalItems: 0,
});
+
const [offers, setOffers] = useState>([]);
- const [selectedFilter, setSelectedFilter] = useState(
- OfferTableFilterOptions[0].value,
- );
+
const { event: gaEvent } = useGoogleAnalytics();
const router = useRouter();
- const { yoeCategory = '' } = router.query;
const [isLoading, setIsLoading] = useState(true);
- useEffect(() => {
- setPagination({
- currentPage: 0,
- numOfItems: 0,
- numOfPages: 0,
- totalItems: 0,
- });
- setIsLoading(true);
- }, [selectedYoe, currency, countryFilter, companyFilter, jobTitleFilter]);
+ const [
+ selectedYoeCategory,
+ setSelectedYoeCategory,
+ isYoeCategoryInitialized,
+ ] = useSearchParamSingle('yoeCategory');
+
+ const [selectedSortBy, setSelectedSortBy, isSortByInitialized] =
+ useSearchParamSingle('sortBy');
+
+ const areFilterParamsInitialized = useMemo(() => {
+ return isYoeCategoryInitialized && isSortByInitialized;
+ }, [isYoeCategoryInitialized, isSortByInitialized]);
+ const { pathname } = router;
useEffect(() => {
- setSelectedYoe(yoeCategory as YOE_CATEGORY);
- event?.preventDefault();
- }, [yoeCategory]);
+ if (areFilterParamsInitialized) {
+ router.replace(
+ {
+ pathname,
+ query: {
+ companyId: companyFilter,
+ companyName,
+ jobTitleId: jobTitleFilter,
+ sortBy: selectedSortBy,
+ yoeCategory: selectedYoeCategory,
+ },
+ },
+ undefined,
+ { shallow: true },
+ );
+ setPagination({
+ currentPage: 0,
+ numOfItems: 0,
+ numOfPages: 0,
+ totalItems: 0,
+ });
+ setIsLoading(true);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [
+ areFilterParamsInitialized,
+ currency,
+ countryFilter,
+ companyFilter,
+ jobTitleFilter,
+ selectedSortBy,
+ selectedYoeCategory,
+ pathname,
+ ]);
+ const { showToast } = useToast();
trpc.useQuery(
[
'offers.list',
@@ -75,14 +111,19 @@ export default function OffersTable({
currency,
limit: NUMBER_OF_OFFERS_IN_PAGE,
offset: pagination.currentPage,
- sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived',
+ sortBy: selectedSortBy ?? '-monthYearReceived',
title: jobTitleFilter,
- yoeCategory: YOE_CATEGORY_PARAM[yoeCategory as string] ?? undefined,
+ yoeCategory: selectedYoeCategory
+ ? YOE_CATEGORY_PARAM[selectedYoeCategory as string]
+ : undefined,
},
],
{
- onError: (err) => {
- alert(err);
+ onError: () => {
+ showToast({
+ title: 'Error loading the page.',
+ variant: 'failure',
+ });
},
onSuccess: (response: GetOffersResponse) => {
setOffers(response.data);
@@ -95,44 +136,26 @@ export default function OffersTable({
function renderFilters() {
return (
-
+
itemValue === selectedYoe,
- )[0].label
+ ({ value: itemValue }) => itemValue === selectedYoeCategory,
+ ).length > 0
+ ? OfferTableYoeOptions.filter(
+ ({ value: itemValue }) => itemValue === selectedYoeCategory,
+ )[0].label
+ : OfferTableYoeOptions[0].label
}
size="inherit">
{OfferTableYoeOptions.map(({ label: itemLabel, value }) => (
{
- if (value === '') {
- router.replace(
- {
- pathname: router.pathname,
- query: undefined,
- },
- undefined,
- // Do not refresh the page
- { shallow: true },
- );
- } else {
- const params = new URLSearchParams({
- ['yoeCategory']: value,
- });
- router.replace(
- {
- pathname: location.pathname,
- search: params.toString(),
- },
- undefined,
- { shallow: true },
- );
- }
+ setSelectedYoeCategory(value);
gaEvent({
action: `offers.table_filter_yoe_category_${value}`,
category: 'engagement',
@@ -157,17 +180,21 @@ export default function OffersTable({
align="end"
label={
OfferTableFilterOptions.filter(
- ({ value: itemValue }) => itemValue === selectedFilter,
- )[0].label
+ ({ value: itemValue }) => itemValue === selectedSortBy,
+ ).length > 0
+ ? OfferTableFilterOptions.filter(
+ ({ value: itemValue }) => itemValue === selectedSortBy,
+ )[0].label
+ : OfferTableFilterOptions[0].label
}
size="inherit">
{OfferTableFilterOptions.map(({ label: itemLabel, value }) => (
{
- setSelectedFilter(value);
+ setSelectedSortBy(value as OfferTableSortByType);
}}
/>
))}
@@ -183,7 +210,9 @@ export default function OffersTable({
'Company',
'Title',
'YOE',
- selectedYoe === YOE_CATEGORY.INTERN ? 'Monthly Salary' : 'Annual TC',
+ selectedYoeCategory === YOE_CATEGORY.INTERN
+ ? 'Monthly Salary'
+ : 'Annual TC',
'Date Offered',
'Actions',
];
@@ -200,13 +229,13 @@ export default function OffersTable({
}
return (
-
+
{columns.map((header, index) => (
) : (
-
+
{renderHeader()}
{offers.map((offer) => (
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/table/types.ts b/apps/portal/src/components/offers/table/types.ts
index 7fb5ad17..e9a8c42e 100644
--- a/apps/portal/src/components/offers/table/types.ts
+++ b/apps/portal/src/components/offers/table/types.ts
@@ -36,25 +36,24 @@ export const OfferTableYoeOptions = [
export const OfferTableFilterOptions = [
{
label: 'Latest Submitted',
- value: 'latest-submitted',
+ value: '-monthYearReceived',
},
{
label: 'Highest Salary',
- value: 'highest-salary',
+ value: '-totalCompensation',
},
{
label: 'Highest YOE first',
- value: 'highest-yoe-first',
+ value: '-totalYoe',
},
{
label: 'Lowest YOE first',
- value: 'lowest-yoe-first',
+ value: '+totalYoe',
},
];
-export const OfferTableSortBy: Record = {
- 'highest-salary': '-totalCompensation',
- 'highest-yoe-first': '-totalYoe',
- 'latest-submitted': '-monthYearReceived',
- 'lowest-yoe-first': '+totalYoe',
-};
+export type OfferTableSortByType =
+ | '-monthYearReceived'
+ | '-totalCompensation'
+ | '-totalYoe'
+ | '+totalYoe';
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/components/resumes/submit-form/ResumeSubmissionGuidelines.tsx b/apps/portal/src/components/resumes/submit-form/ResumeSubmissionGuidelines.tsx
new file mode 100644
index 00000000..e399332e
--- /dev/null
+++ b/apps/portal/src/components/resumes/submit-form/ResumeSubmissionGuidelines.tsx
@@ -0,0 +1,29 @@
+export default function ResumeSubmissionGuidelines() {
+ return (
+
+
+ Submission Guidelines
+
+ Before submitting your resume for review, please review and
+ acknowledge our{' '}
+ submission guidelines stated
+ below.
+
+
+ -
+ Ensure that you do not divulge any of your{' '}
+ personal particulars.
+
+ -
+ Ensure that you do not divulge any{' '}
+
+ company's proprietary and confidential information
+
+ .
+
+ - Proofread your resumes for grammatical/spelling errors.
+
+
+
+ );
+}
diff --git a/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx b/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx
deleted file mode 100644
index 96249537..00000000
--- a/apps/portal/src/components/resumes/submit-form/SubmissionGuidelines.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-export default function SubmissionGuidelines() {
- return (
-
- Submission Guidelines
-
- Before you submit, please review and acknowledge our
- submission guidelines
- stated below.
-
-
- •
- Ensure that you do not divulge any of your{' '}
- personal particulars.
-
-
- •
- Ensure that you do not divulge any{' '}
-
- company's proprietary and confidential information
-
- .
-
-
- •
- Proof-read your resumes to look for grammatical/spelling errors.
-
-
- );
-}
diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts
index 07b37bb0..1d1d2f76 100644
--- a/apps/portal/src/mappers/offers-mappers.ts
+++ b/apps/portal/src/mappers/offers-mappers.ts
@@ -87,6 +87,7 @@ const analysisOfferDtoMapper = (
background?.experiences
?.filter((exp) => exp.company != null)
.map((exp) => exp.company?.name ?? '') ?? [],
+ profileId: offer.profileId,
profileName,
title:
offer.jobType === JobType.FULLTIME
@@ -95,7 +96,10 @@ const analysisOfferDtoMapper = (
totalYoe: background?.totalYoe ?? -1,
};
- if (offer.offersFullTime?.totalCompensation) {
+ if (
+ offer.offersFullTime?.totalCompensation &&
+ offer.jobType === JobType.FULLTIME
+ ) {
analysisOfferDto.income.value =
offer.offersFullTime.totalCompensation.value;
analysisOfferDto.income.currency =
@@ -105,7 +109,10 @@ const analysisOfferDtoMapper = (
offer.offersFullTime.totalCompensation.baseValue;
analysisOfferDto.income.baseCurrency =
offer.offersFullTime.totalCompensation.baseCurrency;
- } else if (offer.offersIntern?.monthlySalary) {
+ } else if (
+ offer.offersIntern?.monthlySalary &&
+ offer.jobType === JobType.INTERN
+ ) {
analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value;
analysisOfferDto.income.currency =
offer.offersIntern.monthlySalary.currency;
@@ -126,7 +133,14 @@ const analysisOfferDtoMapper = (
const analysisUnitDtoMapper = (
analysisUnit: OffersAnalysisUnit & {
- company: Company;
+ analysedOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
topSimilarOffers: Array<
OffersOffer & {
company: Company;
@@ -153,15 +167,51 @@ const analysisUnitDtoMapper = (
>;
},
) => {
- const analysisDto: AnalysisUnit = {
- companyName: analysisUnit.company.name,
+ const { analysedOffer } = analysisUnit;
+ const { jobType } = analysedOffer;
+
+ const analysisUnitDto: AnalysisUnit = {
+ companyId: analysedOffer.companyId,
+ companyName: analysedOffer.company.name,
+ income: valuationDtoMapper({
+ baseCurrency: '',
+ baseValue: -1,
+ currency: '',
+ id: '',
+ value: -1,
+ }),
+ jobType,
noOfOffers: analysisUnit.noOfSimilarOffers,
percentile: analysisUnit.percentile,
+ title:
+ jobType === JobType.FULLTIME && analysedOffer.offersFullTime != null
+ ? analysedOffer.offersFullTime.title
+ : jobType === JobType.INTERN && analysedOffer.offersIntern != null
+ ? analysedOffer.offersIntern.title
+ : '',
topPercentileOffers: analysisUnit.topSimilarOffers.map((offer) =>
analysisOfferDtoMapper(offer),
),
+ totalYoe: analysisUnit.analysedOffer.profile.background?.totalYoe ?? 0,
};
- return analysisDto;
+
+ if (
+ analysedOffer.offersFullTime &&
+ analysedOffer.jobType === JobType.FULLTIME
+ ) {
+ analysisUnitDto.income = valuationDtoMapper(
+ analysedOffer.offersFullTime.totalCompensation,
+ );
+ } else if (
+ analysedOffer.offersIntern &&
+ analysedOffer.jobType === JobType.INTERN
+ ) {
+ analysisUnitDto.income = valuationDtoMapper(
+ analysedOffer.offersIntern.monthlySalary,
+ );
+ }
+
+ return analysisUnitDto;
};
const analysisHighestOfferDtoMapper = (
@@ -190,7 +240,16 @@ export const profileAnalysisDtoMapper = (
| (OffersAnalysis & {
companyAnalysis: Array<
OffersAnalysisUnit & {
- company: Company;
+ analysedOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
topSimilarOffers: Array<
OffersOffer & {
company: Company;
@@ -220,7 +279,16 @@ export const profileAnalysisDtoMapper = (
}
>;
overallAnalysis: OffersAnalysisUnit & {
- company: Company;
+ analysedOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
topSimilarOffers: Array<
OffersOffer & {
company: Company;
@@ -369,13 +437,15 @@ export const experienceDtoMapper = (
experience.location != null
? locationDtoMapper(experience.location)
: null,
- monthlySalary: experience.monthlySalary
- ? valuationDtoMapper(experience.monthlySalary)
- : null,
+ monthlySalary:
+ experience.monthlySalary && experience.jobType === JobType.INTERN
+ ? valuationDtoMapper(experience.monthlySalary)
+ : null,
title: experience.title,
- totalCompensation: experience.totalCompensation
- ? valuationDtoMapper(experience.totalCompensation)
- : null,
+ totalCompensation:
+ experience.totalCompensation && experience.jobType === JobType.FULLTIME
+ ? valuationDtoMapper(experience.totalCompensation)
+ : null,
};
return experienceDto;
};
@@ -460,11 +530,11 @@ export const profileOfferDtoMapper = (
location: locationDtoMapper(offer.location),
monthYearReceived: offer.monthYearReceived,
negotiationStrategy: offer.negotiationStrategy,
- offersFullTime: offer.offersFullTime,
- offersIntern: offer.offersIntern,
+ offersFullTime: null,
+ offersIntern: null,
};
- if (offer.offersFullTime) {
+ if (offer.offersFullTime && offer.jobType === JobType.FULLTIME) {
profileOfferDto.offersFullTime = {
baseSalary:
offer.offersFullTime?.baseSalary != null
@@ -485,7 +555,7 @@ export const profileOfferDtoMapper = (
offer.offersFullTime.totalCompensation,
),
};
- } else if (offer.offersIntern) {
+ } else if (offer.offersIntern && offer.jobType === JobType.INTERN) {
profileOfferDto.offersIntern = {
id: offer.offersIntern.id,
internshipCycle: offer.offersIntern.internshipCycle,
@@ -504,7 +574,18 @@ export const profileDtoMapper = (
| (OffersAnalysis & {
companyAnalysis: Array<
OffersAnalysisUnit & {
- company: Company;
+ analysedOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & {
+ background: OffersBackground | null;
+ };
+ };
topSimilarOffers: Array<
OffersOffer & {
company: Company;
@@ -536,7 +617,16 @@ export const profileDtoMapper = (
}
>;
overallAnalysis: OffersAnalysisUnit & {
- company: Company;
+ analysedOffer: OffersOffer & {
+ company: Company;
+ offersFullTime:
+ | (OffersFullTime & { totalCompensation: OffersCurrency })
+ | null;
+ offersIntern:
+ | (OffersIntern & { monthlySalary: OffersCurrency })
+ | null;
+ profile: OffersProfile & { background: OffersBackground | null };
+ };
topSimilarOffers: Array<
OffersOffer & {
company: Company;
@@ -701,30 +791,27 @@ export const dashboardOfferDtoMapper = (
totalYoe: offer.profile.background?.totalYoe ?? -1,
};
- if (offer.offersFullTime) {
+ if (offer.offersFullTime && offer.jobType === JobType.FULLTIME) {
dashboardOfferDto.income = valuationDtoMapper(
offer.offersFullTime.totalCompensation,
);
if (offer.offersFullTime.baseSalary) {
dashboardOfferDto.baseSalary = valuationDtoMapper(
- offer.offersFullTime.baseSalary
+ offer.offersFullTime.baseSalary,
);
}
if (offer.offersFullTime.bonus) {
- dashboardOfferDto.bonus = valuationDtoMapper(
- offer.offersFullTime.bonus
- );
-
+ dashboardOfferDto.bonus = valuationDtoMapper(offer.offersFullTime.bonus);
}
if (offer.offersFullTime.stocks) {
dashboardOfferDto.stocks = valuationDtoMapper(
- offer.offersFullTime.stocks
+ offer.offersFullTime.stocks,
);
}
- } else if (offer.offersIntern) {
+ } else if (offer.offersIntern && offer.jobType === JobType.INTERN) {
dashboardOfferDto.income = valuationDtoMapper(
offer.offersIntern.monthlySalary,
);
@@ -736,12 +823,12 @@ export const dashboardOfferDtoMapper = (
export const getOffersResponseMapper = (
data: Array,
paging: Paging,
- jobType: JobType
+ jobType: JobType,
) => {
const getOffersResponse: GetOffersResponse = {
data,
jobType,
- paging
+ paging,
};
return getOffersResponse;
};
@@ -817,7 +904,10 @@ const userProfileOfferDtoMapper = (
: offer.offersIntern?.title ?? '',
};
- if (offer.offersFullTime?.totalCompensation) {
+ if (
+ offer.offersFullTime?.totalCompensation &&
+ offer.jobType === JobType.FULLTIME
+ ) {
mappedOffer.income.value = offer.offersFullTime.totalCompensation.value;
mappedOffer.income.currency =
offer.offersFullTime.totalCompensation.currency;
@@ -826,7 +916,10 @@ const userProfileOfferDtoMapper = (
offer.offersFullTime.totalCompensation.baseValue;
mappedOffer.income.baseCurrency =
offer.offersFullTime.totalCompensation.baseCurrency;
- } else if (offer.offersIntern?.monthlySalary) {
+ } else if (
+ offer.offersIntern?.monthlySalary &&
+ offer.jobType === JobType.INTERN
+ ) {
mappedOffer.income.value = offer.offersIntern.monthlySalary.value;
mappedOffer.income.currency = offer.offersIntern.monthlySalary.currency;
mappedOffer.income.id = offer.offersIntern.monthlySalary.id;
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/index.tsx b/apps/portal/src/pages/offers/index.tsx
index 9eaeb955..b1773a28 100644
--- a/apps/portal/src/pages/offers/index.tsx
+++ b/apps/portal/src/pages/offers/index.tsx
@@ -9,14 +9,23 @@ import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import Container from '~/components/shared/Container';
import CountriesTypeahead from '~/components/shared/CountriesTypeahead';
import type { JobTitleType } from '~/components/shared/JobTitles';
+import { JobTitleLabels } from '~/components/shared/JobTitles';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
+import { useSearchParamSingle } from '~/utils/offers/useSearchParam';
+
export default function OffersHomePage() {
- const [jobTitleFilter, setJobTitleFilter] = useState('');
- const [companyFilter, setCompanyFilter] = useState('');
const [countryFilter, setCountryFilter] = useState('');
const { event: gaEvent } = useGoogleAnalytics();
+ const [selectedCompanyName, setSelectedCompanyName] =
+ useSearchParamSingle('companyName');
+ const [selectedCompanyId, setSelectedCompanyId] =
+ useSearchParamSingle('companyId');
+
+ const [selectedJobTitleId, setSelectedJobTitleId] =
+ useSearchParamSingle('jobTitleId');
+
return (
@@ -59,23 +68,32 @@ export default function OffersHomePage() {
offers.
-
+
Viewing offers for
{
if (option) {
- setJobTitleFilter(option.value as JobTitleType);
+ setSelectedJobTitleId(option.id as JobTitleType);
gaEvent({
action: `offers.table_filter_job_title_${option.value}`,
category: 'engagement',
label: 'Filter by job title',
});
} else {
- setJobTitleFilter('');
+ setSelectedJobTitleId(null);
}
}}
/>
@@ -84,16 +102,27 @@ export default function OffersHomePage() {
isLabelHidden={true}
placeholder="All Companies"
textSize="inherit"
+ value={
+ selectedCompanyName
+ ? {
+ id: selectedCompanyId,
+ label: selectedCompanyName,
+ value: selectedCompanyId,
+ }
+ : null
+ }
onSelect={(option) => {
if (option) {
- setCompanyFilter(option.value);
+ setSelectedCompanyId(option.id);
+ setSelectedCompanyName(option.label);
gaEvent({
action: `offers.table_filter_company_${option.value}`,
category: 'engagement',
label: 'Filter by company',
});
} else {
- setCompanyFilter('');
+ setSelectedCompanyId('');
+ setSelectedCompanyName('');
}
}}
/>
@@ -102,9 +131,10 @@ export default function OffersHomePage() {
diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
index 73468325..f15e38f0 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';
@@ -79,6 +79,7 @@ export default function OfferProfile() {
jobTitle: getLabelForJobTitleType(
res.offersFullTime.title as JobTitleType,
),
+ jobType: res.jobType,
location: res.location,
negotiationStrategy: res.negotiationStrategy,
otherComment: res.comments,
@@ -99,6 +100,7 @@ export default function OfferProfile() {
jobTitle: getLabelForJobTitleType(
res.offersIntern!.title as JobTitleType,
),
+ jobType: res.jobType,
location: res.location,
monthlySalary: convertMoneyToString(
res.offersIntern!.monthlySalary,
@@ -187,60 +189,54 @@ export default function OfferProfile() {
}
}
- return (
- <>
- {getProfileQuery.isError && (
-
-
+ return getProfileQuery.isError ? (
+
+
+
+ ) : getProfileQuery.isLoading ? (
+
+ ) : (
+
+
+
- )}
- {getProfileQuery.isLoading && (
-
-
+
- )}
- {!getProfileQuery.isLoading && !getProfileQuery.isError && (
-
- )}
- >
+
+
+
);
}
diff --git a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx
index 35c72796..b8bde4a5 100644
--- a/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx
+++ b/apps/portal/src/pages/offers/submit/result/[offerProfileId].tsx
@@ -1,46 +1,56 @@
+import Error from 'next/error';
import { useRouter } from 'next/router';
+import { useSession } from 'next-auth/react';
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';
import { getProfilePath } from '~/utils/offers/link';
import { trpc } from '~/utils/trpc';
-import type { ProfileAnalysis } from '~/types/offers';
-
export default function OffersSubmissionResult() {
const router = useRouter();
let { offerProfileId, token = '' } = router.query;
offerProfileId = offerProfileId as string;
token = token as string;
const [step, setStep] = useState(0);
- const [analysis, setAnalysis] = useState (null);
+ const { data: session } = useSession();
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 getAnalysis = trpc.useQuery(
- ['offers.analysis.get', { profileId: offerProfileId }],
- {
- onSuccess(data) {
- setAnalysis(data);
- },
- },
- );
+ const checkToken = trpc.useQuery([
+ 'offers.profile.isValidToken',
+ { profileId: offerProfileId, token },
+ ]);
+
+ const getAnalysis = trpc.useQuery([
+ 'offers.analysis.get',
+ { profileId: offerProfileId },
+ ]);
+
+ const isSavedQuery = trpc.useQuery([
+ `offers.profile.isSaved`,
+ { profileId: offerProfileId, userId: session?.user?.id },
+ ]);
const steps = [
- ,
+ ,
,
@@ -67,65 +77,67 @@ export default function OffersSubmissionResult() {
scrollToTop();
}, [step]);
- return (
- <>
- {getAnalysis.isLoading && (
-
-
-
- Loading...
+ return checkToken.isLoading || getAnalysis.isLoading ? (
+
+ ) : checkToken.isError || getAnalysis.isError ? (
+
+ ) : checkToken.isSuccess && !checkToken.data ? (
+
+ ) : (
+
+
+
- )}
- {!getAnalysis.isLoading && (
-
-
-
-
-
+ {steps[step]}
+ {step === 0 && (
+
+ setStep(step + 1)}
/>
-
- {steps[step]}
- {step === 0 && (
-
- setStep(step + 1)}
- />
-
- )}
- {step === 1 && (
-
- setStep(step - 1)}
- />
-
-
- )}
+ )}
+ {step === 1 && (
+
+ setStep(step - 1)}
+ />
+
-
+ )}
- )}
- >
+
+
);
}
diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx
index 935e57c2..cb631866 100644
--- a/apps/portal/src/pages/resumes/submit.tsx
+++ b/apps/portal/src/pages/resumes/submit.tsx
@@ -20,7 +20,8 @@ import {
} from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
-import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
+import ResumeSubmissionGuidelines from '~/components/resumes/submit-form/ResumeSubmissionGuidelines';
+import Container from '~/components/shared/Container';
import loginPageHref from '~/components/shared/loginPageHref';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
@@ -239,203 +240,206 @@ export default function SubmitResumeForm({
Upload a Resume
- {status === 'loading' && (
-
- {' '}
- {' '}
-
- )}
- {status === 'authenticated' && (
-
-
- {/* Reset Dialog component */}
-
- }
- secondaryButton={
- setIsDialogShown(false)}
- />
- }
- title={
- isNewForm
- ? 'Are you sure you want to clear?'
- : 'Are you sure you want to leave?'
- }
- onClose={() => setIsDialogShown(false)}>
- Note that your current input will not be saved!
-
- |