-
-
- {getLabelForJobTitleType(title as JobTitleType)}{' '}
- {`(${JobTypeLabel[jobType]})`}
-
-
{`Company: ${getCompanyDisplayText(company.name, location)}`}
- {level &&
Level: {level}
}
-
-
-
{formatDate(monthYearReceived)}
-
- {jobType === JobType.FULLTIME
- ? `${convertMoneyToString(income)} / year`
- : `${convertMoneyToString(income)} / month`}
-
+ function BottomSection() {
+ return (
+
+
+
+
+ {getLabelForJobTitleType(title as JobTitleType)}{' '}
+ {jobType && <>({JobTypeLabel[jobType]})>}
+
+
+ {company?.name && (
+
+
+ {company.name}
+
+ )}
+ {location && (
+
+
+ {location.cityName}
+
+ )}
+ {level && (
+
+ )}
+
+
+
+
+ {jobType === JobType.FULLTIME
+ ? `${convertMoneyToString(income)} / year`
+ : `${convertMoneyToString(income)} / month`}
+
+
+ {formatDate(monthYearReceived)}
+
+
-
+ );
+ }
+
+ return (
+
+
+
+
);
}
diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx
index eeae57c7..7e0a7d47 100644
--- a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx
+++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionAnalysis.tsx
@@ -33,4 +33,4 @@ export default function OffersSubmissionAnalysis({
)}
);
-}
+}
\ No newline at end of file
diff --git a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx
index a03782dd..a927521f 100644
--- a/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx
+++ b/apps/portal/src/components/offers/offersSubmission/OffersSubmissionForm.tsx
@@ -13,10 +13,12 @@ import BackgroundForm from '~/components/offers/offersSubmission/submissionForm/
import OfferDetailsForm from '~/components/offers/offersSubmission/submissionForm/OfferDetailsForm';
import type {
OfferFormData,
+ OfferPostData,
OffersProfileFormData,
} from '~/components/offers/types';
import type { Month } from '~/components/shared/MonthYearPicker';
+import { Currency } from '~/utils/offers/currency/CurrencyEnum';
import {
cleanObject,
removeEmptyObjects,
@@ -25,17 +27,19 @@ import {
import { getCurrentMonth, getCurrentYear } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
+export const DEFAULT_CURRENCY = Currency.SGD;
+
const defaultOfferValues = {
cityId: '',
comments: '',
companyId: '',
- jobTitle: '',
jobType: JobType.FULLTIME,
monthYearReceived: {
month: getCurrentMonth() as Month,
year: getCurrentYear(),
},
negotiationStrategy: '',
+ title: '',
};
export const defaultFullTimeOfferValues = {
@@ -43,21 +47,17 @@ export const defaultFullTimeOfferValues = {
jobType: JobType.FULLTIME,
offersFullTime: {
baseSalary: {
- currency: 'SGD',
- value: null,
+ currency: DEFAULT_CURRENCY,
},
bonus: {
- currency: 'SGD',
- value: null,
+ currency: DEFAULT_CURRENCY,
},
level: '',
stocks: {
- currency: 'SGD',
- value: null,
+ currency: DEFAULT_CURRENCY,
},
totalCompensation: {
- currency: 'SGD',
- value: null,
+ currency: DEFAULT_CURRENCY,
},
},
};
@@ -66,16 +66,15 @@ export const defaultInternshipOfferValues = {
...defaultOfferValues,
jobType: JobType.INTERN,
offersIntern: {
- internshipCycle: null,
+ internshipCycle: '',
monthlySalary: {
- currency: 'SGD',
- value: null,
+ currency: DEFAULT_CURRENCY,
},
startYear: null,
},
};
-const defaultOfferProfileValues = {
+const defaultOfferProfileValues: OffersProfileFormData = {
background: {
educations: [],
experiences: [{ jobType: JobType.FULLTIME }],
@@ -109,6 +108,7 @@ export default function OffersSubmissionForm({
const pageRef = useRef
(null);
const scrollToTop = () =>
pageRef.current?.scrollTo({ behavior: 'smooth', top: 0 });
+
const formMethods = useForm({
defaultValues: initialOfferProfileValues,
mode: 'all',
@@ -116,7 +116,7 @@ export default function OffersSubmissionForm({
const {
handleSubmit,
trigger,
- formState: { isSubmitting },
+ formState: { isSubmitting, isDirty },
} = formMethods;
const generateAnalysisMutation = trpc.useMutation(
@@ -218,7 +218,7 @@ export default function OffersSubmissionForm({
offer.monthYearReceived.year,
offer.monthYearReceived.month - 1, // Convert month to monthIndex
),
- }));
+ })) as Array;
if (params.profileId && params.token) {
createOrUpdateMutation.mutate({
@@ -254,11 +254,14 @@ export default function OffersSubmissionForm({
const warningText =
'Leave this page? Changes that you made will not be saved.';
const handleWindowClose = (e: BeforeUnloadEvent) => {
+ if (!isDirty) {
+ return;
+ }
e.preventDefault();
return (e.returnValue = warningText);
};
const handleRouteChange = (url: string) => {
- if (url.includes('/offers/submit/result')) {
+ if (url.includes('/offers/submit/result') || !isDirty) {
return;
}
if (window.confirm(warningText)) {
@@ -274,7 +277,7 @@ export default function OffersSubmissionForm({
router.events.off('routeChangeStart', handleRouteChange);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ }, [isDirty]);
return generateAnalysisMutation.isLoading ? (
diff --git a/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx
index d2cef0a7..63e0148b 100644
--- a/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx
+++ b/apps/portal/src/components/offers/offersSubmission/submissionForm/BackgroundForm.tsx
@@ -4,11 +4,6 @@ import { Collapsible, RadioList } from '@tih/ui';
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';
-import type { JobTitleType } from '~/components/shared/JobTitles';
-import { getLabelForJobTitleType } from '~/components/shared/JobTitles';
-import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
import {
Currency,
@@ -17,6 +12,9 @@ import {
import { EducationFieldOptions } from '../../EducationFields';
import { EducationLevelOptions } from '../../EducationLevels';
+import FormCitiesTypeahead from '../../forms/FormCitiesTypeahead';
+import FormCompaniesTypeahead from '../../forms/FormCompaniesTypeahead';
+import FormJobTitlesTypeahead from '../../forms/FormJobTitlesTypeahead';
import FormRadioList from '../../forms/FormRadioList';
import FormSection from '../../forms/FormSection';
import FormSelect from '../../forms/FormSelect';
@@ -85,56 +83,19 @@ function YoeSection() {
}
function FullTimeJobFields() {
- const { register, setValue, formState } = useFormContext<{
+ const { register, formState } = useFormContext<{
background: BackgroundPostData;
}>();
const experiencesField = formState.errors.background?.experiences?.[0];
- const watchJobTitle = useWatch({
- name: 'background.experiences.0.title',
- });
- const watchCompanyId = useWatch({
- name: 'background.experiences.0.companyId',
- });
- const watchCompanyName = useWatch({
- name: 'background.experiences.0.companyName',
- });
- const watchCityId = useWatch({
- name: 'background.experiences.0.cityId',
- });
- const watchCityName = useWatch({
- name: 'background.experiences.0.cityName',
- });
-
return (
<>
- {
- if (option) {
- setValue('background.experiences.0.title', option.value);
- }
- }}
- />
- {
- 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', '');
- }
+
+
@@ -172,21 +133,10 @@ function FullTimeJobFields() {
placeholder="e.g. L4, Junior"
{...register(`background.experiences.0.level`)}
/>
- {
- 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', '');
- }
+
();
const experiencesField = formState.errors.background?.experiences?.[0];
- const watchJobTitle = useWatch({
- name: 'background.experiences.0.title',
- });
- const watchCompanyId = useWatch({
- name: 'background.experiences.0.companyId',
- });
- const watchCompanyName = useWatch({
- name: 'background.experiences.0.companyName',
- });
- const watchCityId = useWatch({
- name: 'background.experiences.0.cityId',
- });
- const watchCityName = useWatch({
- name: 'background.experiences.0.cityName',
- });
-
return (
<>
- {
- if (option) {
- setValue('background.experiences.0.title', option.value);
- }
- }}
- />
- {
- if (option) {
- setValue('background.experiences.0.companyId', option.value);
- setValue('background.experiences.0.companyName', option.label);
- }
+
+
@@ -280,21 +196,10 @@ 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', '');
- }
+
;
}>();
const offerFields = formState.errors.offers?.[index];
- const watchJobTitle = useWatch({
- name: `offers.${index}.offersFullTime.title`,
- });
- const watchCompanyId = useWatch({
- name: `offers.${index}.companyId`,
- });
- const watchCompanyName = useWatch({
- name: `offers.${index}.companyName`,
- });
- const watchCityId = useWatch({
- name: `offers.${index}.cityId`,
- });
- const watchCityName = useWatch({
- name: `offers.${index}.cityName`,
- });
const watchCurrency = useWatch({
name: `offers.${index}.offersFullTime.totalCompensation.currency`,
});
@@ -83,18 +66,17 @@ function FullTimeOfferDetailsForm({
- {
- if (option) {
- setValue(`offers.${index}.offersFullTime.title`, option.value);
- }
- }}
+ (
+
+ )}
+ rules={{ required: true }}
/>
- {
- if (option) {
- setValue(`offers.${index}.companyId`, option.value);
- setValue(`offers.${index}.companyName`, option.label);
- }
- }}
+ (
+
+ )}
+ rules={{ required: true }}
/>
- {
- if (option) {
- setValue(`offers.${index}.cityId`, option.value);
- setValue(`offers.${index}.cityName`, option.label);
- } else {
- setValue(`offers.${index}.cityId`, '');
- setValue(`offers.${index}.cityName`, '');
- }
- }}
+ (
+
+ )}
+ rules={{ required: true }}
/>
@@ -303,76 +283,56 @@ function InternshipOfferDetailsForm({
index,
remove,
}: InternshipOfferDetailsFormProps) {
- const { register, formState, setValue } = useFormContext<{
+ const { register, formState, control } = useFormContext<{
offers: Array
;
}>();
const offerFields = formState.errors.offers?.[index];
- const watchJobTitle = useWatch({
- name: `offers.${index}.offersIntern.title`,
- });
- const watchCompanyId = useWatch({
- name: `offers.${index}.companyId`,
- });
- const watchCompanyName = useWatch({
- name: `offers.${index}.companyName`,
- });
- const watchCityId = useWatch({
- name: `offers.${index}.cityId`,
- });
- const watchCityName = useWatch({
- name: `offers.${index}.cityName`,
- });
-
return (
- {
- if (option) {
- setValue(`offers.${index}.offersIntern.title`, option.value);
- }
- }}
+ (
+
+ )}
+ rules={{ required: true }}
/>
-
- {
- if (option) {
- setValue(`offers.${index}.companyId`, option.value);
- setValue(`offers.${index}.companyName`, option.label);
- }
- }}
+ (
+
+ )}
+ rules={{ required: true }}
/>
- {
- if (option) {
- setValue(`offers.${index}.cityId`, option.value);
- setValue(`offers.${index}.cityName`, option.label);
- } else {
- setValue(`offers.${index}.cityId`, '');
- setValue(`offers.${index}.cityName`, '');
- }
- }}
+ (
+
+ )}
+ rules={{ required: true }}
/>
@@ -551,7 +511,6 @@ export default function OfferDetailsForm() {
if (newJobType === jobType) {
return;
}
-
setDialogOpen(true);
}}
/>
diff --git a/apps/portal/src/components/offers/profile/OfferCard.tsx b/apps/portal/src/components/offers/profile/OfferCard.tsx
index 42212fce..2ed305f3 100644
--- a/apps/portal/src/components/offers/profile/OfferCard.tsx
+++ b/apps/portal/src/components/offers/profile/OfferCard.tsx
@@ -169,4 +169,4 @@ export default function OfferCard({
);
-}
+}
\ No newline at end of file
diff --git a/apps/portal/src/components/offers/profile/ProfileComments.tsx b/apps/portal/src/components/offers/profile/ProfileComments.tsx
index 0eeaa1a3..a9357d92 100644
--- a/apps/portal/src/components/offers/profile/ProfileComments.tsx
+++ b/apps/portal/src/components/offers/profile/ProfileComments.tsx
@@ -186,7 +186,7 @@ export default function ProfileComments({
display="block"
isLabelHidden={false}
isLoading={createCommentMutation.isLoading}
- label="Comment"
+ label="Submit"
size="sm"
variant="primary"
onClick={() => handleComment(currentReply)}
diff --git a/apps/portal/src/components/offers/profile/ProfileDetails.tsx b/apps/portal/src/components/offers/profile/ProfileDetails.tsx
index 1d606c03..3be4ae3c 100644
--- a/apps/portal/src/components/offers/profile/ProfileDetails.tsx
+++ b/apps/portal/src/components/offers/profile/ProfileDetails.tsx
@@ -131,9 +131,8 @@ function ProfileAnalysis({
{isEditable && (
generateAnalysisMutation.mutate({ profileId })}
/>
diff --git a/apps/portal/src/components/offers/profile/comments/CommentCard.tsx b/apps/portal/src/components/offers/profile/comments/CommentCard.tsx
index b958d329..7c198d32 100644
--- a/apps/portal/src/components/offers/profile/comments/CommentCard.tsx
+++ b/apps/portal/src/components/offers/profile/comments/CommentCard.tsx
@@ -1,10 +1,10 @@
+import { formatDistanceToNow } from 'date-fns';
import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import { Button, Dialog, TextArea, useToast } from '@tih/ui';
import ProfilePhotoHolder from '~/components/offers/profile/ProfilePhotoHolder';
-import { timeSinceNow } from '~/utils/offers/time';
import { trpc } from '~/utils/trpc';
import type { Reply } from '~/types/offers';
@@ -135,45 +135,45 @@ export default function CommentCard({
)}
-
-
- {user?.name ?? 'unknown user'}
+
+
+ {user?.name ?? 'Unknown user'}
+
·
+
+ {formatDistanceToNow(createdAt, {
+ addSuffix: true,
+ })}
+
-
-
- {timeSinceNow(createdAt)} ago
- {' '}
- {replyLength > 0 && (
- <>
-
· {' '}
-
- {isExpanded ? `Hide replies` : `View replies (${replyLength})`}
-
- >
- )}
+
{!disableReply && (
- <>
- · {' '}
- setIsReplying(!isReplying)}>
- Reply
-
- >
+ setIsReplying(!isReplying)}>
+ Reply
+
+ )}
+ {replyLength > 0 && (
+
+ {isExpanded
+ ? `Hide ${replyLength === 1 ? 'reply' : 'replies'}`
+ : `Show ${replyLength} ${
+ replyLength === 1 ? 'reply' : 'replies'
+ }`}
+
)}
{deletable && (
<>
- · {' '}
setIsDialogOpen(true)}>
@@ -210,8 +210,9 @@ export default function CommentCard({
)}
{!disableReply && isReplying && (
-
+
diff --git a/apps/portal/src/components/offers/table/types.ts b/apps/portal/src/components/offers/table/types.ts
index e9a8c42e..1be3d1e7 100644
--- a/apps/portal/src/components/offers/table/types.ts
+++ b/apps/portal/src/components/offers/table/types.ts
@@ -56,4 +56,4 @@ export type OfferTableSortByType =
| '-monthYearReceived'
| '-totalCompensation'
| '-totalYoe'
- | '+totalYoe';
+ | '+totalYoe';
\ No newline at end of file
diff --git a/apps/portal/src/components/offers/types.ts b/apps/portal/src/components/offers/types.ts
index b38d1540..dcf61944 100644
--- a/apps/portal/src/components/offers/types.ts
+++ b/apps/portal/src/components/offers/types.ts
@@ -4,16 +4,99 @@ import type { MonthYear } from '~/components/shared/MonthYearPicker';
import type { Location } from '~/types/offers';
-export type OffersProfilePostData = {
- background: BackgroundPostData;
+/**
+ * Form data types
+ */
+
+export type OffersProfileFormData = {
+ background: BackgroundFormData;
id?: string;
- offers: Array
;
+ offers: Array;
};
-export type OffersProfileFormData = {
+export type BackgroundFormData = {
+ educations: Array;
+ experiences: Array;
+ id?: string;
+ specificYoes: Array;
+ totalYoe: number;
+};
+
+type EducationFormData = {
+ endDate?: Date | null;
+ field?: string | null;
+ school?: string | null;
+ startDate?: Date | null;
+ type?: string | null;
+};
+
+type ExperienceFormData = {
+ cityId?: string | null;
+ cityName?: string | null;
+ companyId?: string | null;
+ companyName?: string | null;
+ durationInMonths?: number | null;
+ id?: string;
+ jobType?: string | null;
+ level?: string | null;
+ monthlySalary?: MoneyFormData | null;
+ title?: string | null;
+ totalCompensation?: MoneyFormData | null;
+ totalCompensationId?: string | null;
+};
+
+type SpecificYoeFormData = {
+ domain: string;
+ id?: string;
+ yoe: number;
+};
+
+export type OfferFormData = {
+ cityId: string;
+ cityName?: string;
+ comments: string;
+ companyId: string;
+ companyName?: string;
+ id?: string;
+ jobType: JobType;
+ monthYearReceived: MonthYear;
+ negotiationStrategy: string;
+ offersFullTime?: OfferFullTimeFormData | null;
+ offersIntern?: OfferInternFormData | null;
+};
+
+export type OfferFullTimeFormData = {
+ baseSalary?: MoneyFormData | null;
+ bonus?: MoneyFormData | null;
+ id?: string;
+ level: string;
+ stocks?: MoneyFormData | null;
+ title: string;
+ totalCompensation: MoneyFormData;
+};
+
+export type OfferInternFormData = {
+ id?: string;
+ internshipCycle: string;
+ monthlySalary: MoneyFormData;
+ startYear: number;
+ title: string;
+};
+
+type MoneyFormData = {
+ currency: string;
+ id?: string;
+ value?: number;
+};
+
+/**
+ * Post request data types
+ */
+
+export type OffersProfilePostData = {
background: BackgroundPostData;
id?: string;
- offers: Array;
+ offers: Array;
};
export type BackgroundPostData = {
@@ -24,6 +107,8 @@ export type BackgroundPostData = {
totalYoe: number;
};
+type EducationPostData = EducationFormData;
+
type ExperiencePostData = {
cityId?: string | null;
cityName?: string | null;
@@ -39,47 +124,26 @@ type ExperiencePostData = {
totalCompensationId?: string | null;
};
-type EducationPostData = {
- endDate?: Date | null;
- field?: string | null;
- id?: string;
- school?: string | null;
- startDate?: Date | null;
- type?: string | null;
-};
-
-type SpecificYoePostData = {
- domain: string;
- id?: string;
- yoe: number;
-};
-
-type SpecificYoe = SpecificYoePostData;
+type SpecificYoePostData = SpecificYoeFormData;
export type OfferPostData = {
cityId: string;
- cityName?: string;
comments: string;
companyId: string;
- companyName?: string;
id?: string;
jobType: JobType;
monthYearReceived: Date;
negotiationStrategy: string;
- offersFullTime?: OfferFullTimePostData | null;
- offersIntern?: OfferInternPostData | null;
-};
-
-export type OfferFormData = Omit & {
- monthYearReceived: MonthYear;
+ offersFullTime?: OfferFullTimePostData;
+ offersIntern?: OfferInternPostData;
};
export type OfferFullTimePostData = {
- baseSalary: Money | null;
- bonus: Money | null;
+ baseSalary: Money;
+ bonus: Money;
id?: string;
level: string;
- stocks: Money | null;
+ stocks: Money;
title: string;
totalCompensation: Money;
};
@@ -98,6 +162,10 @@ export type Money = {
value: number;
};
+/**
+ * Display data types
+ */
+
export type EducationDisplayData = {
endDate?: string | null;
field?: string | null;
@@ -128,7 +196,7 @@ export type BackgroundDisplayData = {
educations: Array;
experiences: Array;
profileName: string;
- specificYoes: Array;
+ specificYoes: Array;
totalYoe: number;
};
@@ -141,4 +209,4 @@ export type CommentEntity = {
replyingToId: string;
userId: string;
username: string;
-};
+};
\ No newline at end of file
diff --git a/apps/portal/src/components/questions/AddToListDropdown.tsx b/apps/portal/src/components/questions/AddToListDropdown.tsx
index 4ae9da46..039edd7e 100644
--- a/apps/portal/src/components/questions/AddToListDropdown.tsx
+++ b/apps/portal/src/components/questions/AddToListDropdown.tsx
@@ -3,22 +3,52 @@ import type { PropsWithChildren } from 'react';
import { useMemo } from 'react';
import { Fragment, useRef, useState } from 'react';
import { Menu, Transition } from '@headlessui/react';
-import { CheckIcon, HeartIcon } from '@heroicons/react/20/solid';
+import { CheckIcon, HeartIcon, PlusIcon } from '@heroicons/react/20/solid';
+import {
+ useAddQuestionToListAsync,
+ useCreateListAsync,
+ useRemoveQuestionFromListAsync,
+} from '~/utils/questions/mutations';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
import { trpc } from '~/utils/trpc';
+import CreateListDialog from './CreateListDialog';
+
export type AddToListDropdownProps = {
questionId: string;
};
+export type DropdownButtonProps = PropsWithChildren<{
+ onClick: () => void;
+}>;
+
+function DropdownButton({ onClick, children }: DropdownButtonProps) {
+ return (
+
+ {({ active }) => (
+
+ {children}
+
+ )}
+
+ );
+}
+
export default function AddToListDropdown({
questionId,
}: AddToListDropdownProps) {
const [menuOpened, setMenuOpened] = useState(false);
const ref = useRef(null);
+ const [show, setShow] = useState(false);
- const utils = trpc.useContext();
+ const createListAsync = useCreateListAsync();
const { data: lists } = trpc.useQuery(['questions.lists.getListsByUser']);
const listsWithQuestionData = useMemo(() => {
@@ -30,25 +60,8 @@ export default function AddToListDropdown({
}));
}, [lists, questionId]);
- const { mutateAsync: addQuestionToList } = trpc.useMutation(
- 'questions.lists.createQuestionEntry',
- {
- // TODO: Add optimistic update
- onSuccess: () => {
- utils.invalidateQueries(['questions.lists.getListsByUser']);
- },
- },
- );
-
- const { mutateAsync: removeQuestionFromList } = trpc.useMutation(
- 'questions.lists.deleteQuestionEntry',
- {
- // TODO: Add optimistic update
- onSuccess: () => {
- utils.invalidateQueries(['questions.lists.getListsByUser']);
- },
- },
- );
+ const addQuestionToList = useAddQuestionToListAsync();
+ const removeQuestionFromList = useRemoveQuestionFromListAsync();
const addClickOutsideListener = () => {
document.addEventListener('click', handleClickOutside, true);
@@ -101,63 +114,79 @@ export default function AddToListDropdown({
);
return (
-
-
-
-
- Add to List
-
-
-
-
-
- {menuOpened && (
- <>
- {(listsWithQuestionData ?? []).map((list) => (
-
-
- {({ active }) => (
- {
- if (list.hasQuestion) {
- handleDeleteFromList(list.id);
- } else {
- handleAddToList(list.id);
- }
- }}>
+
+
+
+
+
+ Add to list
+
+
+
+
+ {menuOpened && (
+ <>
+ {(listsWithQuestionData ?? []).map((list) => (
+
+
{
+ if (list.hasQuestion) {
+ handleDeleteFromList(list.id);
+ } else {
+ handleAddToList(list.id);
+ }
+ }}>
+
+
+ {list.name}
+
{list.hasQuestion && (
)}
- {list.name}
-
- )}
-
-
- ))}
- >
- )}
-
-
-
+
+
+
+ ))}
+ {
+ setShow(true);
+ }}>
+
+
+ Create new list
+
+
+ >
+ )}
+
+
+
+ {
+ setShow(false);
+ }}
+ onSubmit={async (data) => {
+ await createListAsync(data);
+ setShow(false);
+ }}
+ />
+
);
}
diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx
index f91e1eb4..be050d96 100644
--- a/apps/portal/src/components/questions/ContributeQuestionCard.tsx
+++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx
@@ -1,9 +1,4 @@
import { useState } from 'react';
-import {
- BuildingOffice2Icon,
- CalendarDaysIcon,
- QuestionMarkCircleIcon,
-} from '@heroicons/react/24/outline';
import { TextInput } from '@tih/ui';
import { useProtectedCallback } from '~/utils/questions/useProtectedCallback';
@@ -32,44 +27,19 @@ export default function ContributeQuestionCard({
return (
-
+
+
+
-
-
-
-
-
-
-
-
-
Contribute
diff --git a/apps/portal/src/components/questions/QuestionsNavigation.tsx b/apps/portal/src/components/questions/QuestionsNavigation.tsx
index 363bdf7e..9ad8b850 100644
--- a/apps/portal/src/components/questions/QuestionsNavigation.tsx
+++ b/apps/portal/src/components/questions/QuestionsNavigation.tsx
@@ -8,8 +8,7 @@ const navigation: ProductNavigationItems = [
];
const config = {
- // TODO: Change this to your own GA4 measurement ID.
- googleAnalyticsMeasurementID: 'G-DBLZDQ2ZZN',
+ googleAnalyticsMeasurementID: 'G-0T4LYWMK8L',
logo: (
),
diff --git a/apps/portal/src/components/questions/SortOptionsSelect.tsx b/apps/portal/src/components/questions/SortOptionsSelect.tsx
index 17e555f7..9a9efb64 100644
--- a/apps/portal/src/components/questions/SortOptionsSelect.tsx
+++ b/apps/portal/src/components/questions/SortOptionsSelect.tsx
@@ -8,16 +8,15 @@ export type SortOption
= {
value: Value;
};
-const sortTypeOptions = SORT_TYPES;
-const sortOrderOptions = SORT_ORDERS;
-
type SortOrderProps = {
onSortOrderChange?: (sortValue: Order) => void;
+ sortOrderOptions?: Array>;
sortOrderValue: Order;
};
type SortTypeProps = {
onSortTypeChange?: (sortType: Type) => void;
+ sortTypeOptions?: Array>;
sortTypeValue: Type;
};
@@ -29,17 +28,22 @@ export default function SortOptionsSelect({
sortOrderValue,
onSortTypeChange,
sortTypeValue,
+ sortOrderOptions,
+ sortTypeOptions,
}: SortOptionsSelectProps) {
+ const sortTypes = sortTypeOptions ?? SORT_TYPES;
+ const sortOrders = sortOrderOptions ?? SORT_ORDERS;
+
return (
{
- const chosenOption = sortTypeOptions.find(
+ const chosenOption = sortTypes.find(
(option) => String(option.value) === value,
);
if (chosenOption) {
@@ -52,10 +56,10 @@ export default function SortOptionsSelect({
{
- const chosenOption = sortOrderOptions.find(
+ const chosenOption = sortOrders.find(
(option) => String(option.value) === value,
);
if (chosenOption) {
diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx
index 7b50c1a9..ee3b0630 100644
--- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx
+++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx
@@ -90,7 +90,7 @@ type ReceivedStatisticsProps =
type CreateEncounterProps =
| {
createEncounterButtonText: string;
- onReceivedSubmit: (data: CreateQuestionEncounterData) => void;
+ onReceivedSubmit: (data: CreateQuestionEncounterData) => Promise;
showCreateEncounterButton: true;
}
| {
@@ -185,7 +185,7 @@ export default function BaseQuestionCard({
)}
-
+
{showAggregateStatistics && (
<>
@@ -263,9 +263,8 @@ export default function BaseQuestionCard({
onCancel={() => {
setShowReceivedForm(false);
}}
- onSubmit={(data) => {
- onReceivedSubmit?.(data);
- setShowReceivedForm(false);
+ onSubmit={async (data) => {
+ await onReceivedSubmit?.(data);
}}
/>
)}
diff --git a/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx b/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx
index 10aa33ef..bfbca030 100644
--- a/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx
+++ b/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx
@@ -5,7 +5,7 @@ import { ArrowPathIcon } from '@heroicons/react/20/solid';
import type { QuestionsQuestionType } from '@prisma/client';
import type { TypeaheadOption } from '@tih/ui';
import { CheckboxInput } from '@tih/ui';
-import { Button, HorizontalDivider, Select, TextArea } from '@tih/ui';
+import { Button, Select, TextArea } from '@tih/ui';
import { QUESTION_TYPES } from '~/utils/questions/constants';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
@@ -187,11 +187,9 @@ export default function ContributeQuestionForm({
/>
-
-
-
+
Are these questions the same as yours?
@@ -243,11 +241,13 @@ export default function ContributeQuestionForm({
/>
);
})}
- {similarQuestions?.length === 0 && (
-
- No similar questions found.
-
- )}
+ {similarQuestions?.length === 0 &&
+ contentToCheck?.length !== 0 &&
+ questionContent === contentToCheck && (
+
+ No similar questions found.
+
+ )}
void;
- onSubmit: (data: CreateQuestionEncounterData) => void;
+ onSubmit: (data: CreateQuestionEncounterData) => Promise
;
};
export default function CreateQuestionEncounterForm({
@@ -30,6 +31,8 @@ export default function CreateQuestionEncounterForm({
onSubmit,
}: CreateQuestionEncounterFormProps) {
const [step, setStep] = useState(0);
+ const [loading, setLoading] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
const [selectedCompany, setSelectedCompany] = useState(null);
const [selectedLocation, setSelectedLocation] = useState(
@@ -40,9 +43,18 @@ export default function CreateQuestionEncounterForm({
startOfMonth(new Date()),
);
+ if (submitted) {
+ return (
+
+
+
Thank you for your response
+
+ );
+ }
+
return (
-
+
I saw this question {step <= 1 ? 'at' : step === 2 ? 'for' : 'on'}
{step === 0 && (
@@ -128,9 +140,10 @@ export default function CreateQuestionEncounterForm({
)}
{step === 3 && (
{
+ onClick={async () => {
if (
selectedCompany &&
selectedLocation &&
@@ -138,14 +151,20 @@ export default function CreateQuestionEncounterForm({
selectedDate
) {
const { cityId, stateId, countryId } = selectedLocation;
- onSubmit({
- cityId,
- company: selectedCompany,
- countryId,
- role: selectedRole,
- seenAt: selectedDate,
- stateId,
- });
+ setLoading(true);
+ try {
+ await onSubmit({
+ cityId,
+ company: selectedCompany,
+ countryId,
+ role: selectedRole,
+ seenAt: selectedDate,
+ stateId,
+ });
+ setSubmitted(true);
+ } finally {
+ setLoading(false);
+ }
}
}}
/>
diff --git a/apps/portal/src/components/resumes/ResumePdf.tsx b/apps/portal/src/components/resumes/ResumePdf.tsx
index 8caf0cb5..ea0bddba 100644
--- a/apps/portal/src/components/resumes/ResumePdf.tsx
+++ b/apps/portal/src/components/resumes/ResumePdf.tsx
@@ -35,10 +35,12 @@ export default function ResumePdf({ url }: Props) {
}, [pageWidth]);
return (
-
-
+
+
}
noData=""
@@ -79,7 +81,7 @@ export default function ResumePdf({ url }: Props) {
-
+
badge.isValid(payload));
+
+ if (badges.length === 0) {
+ return null;
+ }
+
return (
- {RESUME_USER_BADGES.filter((badge) => badge.isValid(payload)).map(
- (badge) => (
-
- ),
- )}
+ {badges.map((badge) => (
+
+ ))}
);
}
diff --git a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx
index a9d1731c..daa36dca 100644
--- a/apps/portal/src/components/resumes/browse/ResumeListItem.tsx
+++ b/apps/portal/src/components/resumes/browse/ResumeListItem.tsx
@@ -12,17 +12,7 @@ import { ChatBubbleLeftIcon, StarIcon } from '@heroicons/react/24/outline';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
-import type {
- ExperienceFilter,
- LocationFilter,
- RoleFilter,
-} from '~/utils/resumes/resumeFilters';
-import {
- EXPERIENCES,
- getFilterLabel,
- LOCATIONS,
- ROLES,
-} from '~/utils/resumes/resumeFilters';
+import { getFilterLabel } from '~/utils/resumes/resumeFilters';
import type { Resume } from '~/types/resume';
@@ -47,15 +37,14 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
- {resumeInfo.title}
+
{resumeInfo.title}
-
- {resumeInfo.isResolved ? 'Reviewed' : 'Unreviewed'}
-
+ {resumeInfo.isResolved ? 'Reviewed' : 'Unreviewed'}
@@ -64,17 +53,14 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
aria-hidden="true"
className="mr-1.5 h-4 w-4 flex-shrink-0"
/>
- {getFilterLabel(ROLES, resumeInfo.role as RoleFilter)}
+ {getFilterLabel('role', resumeInfo.role)}
- {getFilterLabel(
- EXPERIENCES,
- resumeInfo.experience as ExperienceFilter,
- )}
+ {getFilterLabel('experience', resumeInfo.experience)}
@@ -102,9 +88,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
addSuffix: true,
})} by ${resumeInfo.user}`}
-
- {getFilterLabel(LOCATIONS, resumeInfo.location as LocationFilter)}
-
+
{resumeInfo.location}
diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx
index 98384595..7db38ac9 100644
--- a/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx
+++ b/apps/portal/src/components/resumes/comments/ResumeCommentListItem.tsx
@@ -1,7 +1,6 @@
import clsx from 'clsx';
import { formatDistanceToNow } from 'date-fns';
import { useState } from 'react';
-import { ChevronUpIcon } from '@heroicons/react/20/solid';
import { FaceSmileIcon } from '@heroicons/react/24/outline';
import ResumeCommentEditForm from './comment/ResumeCommentEditForm';
@@ -12,10 +11,10 @@ import ResumeExpandableText from '../shared/ResumeExpandableText';
import type { ResumeComment } from '~/types/resume-comments';
-type ResumeCommentListItemProps = {
+type ResumeCommentListItemProps = Readonly<{
comment: ResumeComment;
userId: string | undefined;
-};
+}>;
export default function ResumeCommentListItem({
comment,
@@ -28,14 +27,14 @@ export default function ResumeCommentListItem({
return (
-
+
{/* Image Icon */}
{comment.user.image ? (
@@ -50,24 +49,18 @@ export default function ResumeCommentListItem({
{/* Name and creation time */}
-
-
-
- {comment.user.name ?? 'Reviewer ABC'}
-
-
-
- {isCommentOwner ? '(Me)' : ''}
-
-
-
-
-
-
+
+
+ {comment.user.name ?? 'Reviewer ABC'}
+
+ {isCommentOwner && (
+
+ Me
+
+ )}
+
+
·
+
{formatDistanceToNow(comment.createdAt, {
addSuffix: true,
})}
@@ -81,7 +74,7 @@ export default function ResumeCommentListItem({
setIsEditingComment={setIsEditingComment}
/>
) : (
-
+
+
-
{/* Action buttons; only present for authenticated user when not editing/replying */}
- {userId && !isEditingComment && !isReplyingComment && (
- <>
- {isCommentOwner && (
- setIsEditingComment(true)}>
- Edit
-
- )}
-
- {!comment.parentId && (
- setIsReplyingComment(true)}>
- Reply
-
- )}
- >
+ {userId && !comment.parentId && (
+ {
+ setIsReplyingComment(!isReplyingComment);
+ setIsEditingComment(false);
+ }}>
+ Reply
+
)}
-
-
- {/* Reply Form */}
- {isReplyingComment && (
-
- )}
-
- {/* Replies */}
- {comment.children.length > 0 && (
-
+ {comment.children.length > 0 && (
setShowReplies(!showReplies)}>
-
{showReplies
? `Hide ${
@@ -150,26 +112,48 @@ export default function ResumeCommentListItem({
}`}
+ )}
+ {isCommentOwner && (
+ {
+ setIsEditingComment(!isEditingComment);
+ setIsReplyingComment(false);
+ }}>
+ Edit
+
+ )}
+
- {showReplies && (
-
-
+ {/* Reply Form */}
+ {isReplyingComment && (
+
+
+
+ )}
-
- {comment.children.map((child) => {
- return (
-
- );
- })}
-
+ {/* Replies */}
+ {comment.children.length > 0 && showReplies && (
+
+
+
+ {comment.children.map((child) => {
+ return (
+
+ );
+ })}
- )}
+
)}
diff --git a/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx b/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx
index d15cf82a..3a12a41f 100644
--- a/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx
+++ b/apps/portal/src/components/resumes/comments/ResumeCommentsForm.tsx
@@ -115,10 +115,12 @@ export default function ResumeCommentsForm({
};
return (
-
-
Add your review
-
- Please fill in at least one section to submit your review
+
+
+ Contribute a review
+
+
+ Please fill in at least one section to submit a review.