-
+
+
+ {/* Hero section */}
+
+
+
+ Choosing offers
+
+ made easier
+
+
+
+ Analyze your offers using profiles from fellow software engineers.
+
+
- in
-
- setCompanyFilter(value)}
- />
+
+
+
+ {/* Alternating Feature Sections */}
+
+
+
+
+ }
+ imageAlt="Offer table page"
+ imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
+ title="Choosing an offer needs context"
+ />
+
+
+
+ }
+ imageAlt="Customer profile user interface"
+ imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-2.jpg"
+ title="Better understand your offers"
+ />
+
+
+
+ }
+ imageAlt="Offer table page"
+ imageSrc="https://tailwindui.com/img/component-images/inbox-app-screenshot-1.jpg"
+ title="Stay informed of recent offers"
+ />
+
+
+
+ {/* Gradient Feature Section */}
+
+
+
+ Your privacy is our priority.
+
+
+ All offer profiles are anonymized and we do not store information
+ about your personal identity.
+
+
+ {features.map((feature) => (
+
+
+
+
+
+
+
+
+ {feature.name}
+
+
+ {feature.description}
+
+
+
+ ))}
+
+
+
+
+ {/* CTA Section */}
+
+
+
+ Ready to get started?
+
+ Create your own offer profile today.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ © 2022 Tech Interview Handbook Offer Profile Repository. All
+ rights reserved.
+
-
-
-
-
-
+
+
);
}
diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
index ae9956b0..7cf2811d 100644
--- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
+++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
@@ -10,6 +10,9 @@ 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';
import { useToast } from '~/../../../packages/ui/dist';
import { convertMoneyToString } from '~/utils/offers/currency';
@@ -44,7 +47,7 @@ export default function OfferProfile() {
enabled: typeof offerProfileId === 'string',
onSuccess: (data: Profile) => {
if (!data) {
- router.push('/offers');
+ router.push(HOME_URL);
}
// If the profile is not editable with a wrong token, redirect to the profile page
if (!data?.isEditable && token !== '') {
@@ -57,17 +60,28 @@ export default function OfferProfile() {
? data?.offers.map((res: ProfileOffer) => {
if (res.offersFullTime) {
const filteredOffer: OfferDisplayData = {
- base: convertMoneyToString(res.offersFullTime.baseSalary),
- bonus: convertMoneyToString(res.offersFullTime.bonus),
+ base:
+ res.offersFullTime.baseSalary != null
+ ? convertMoneyToString(res.offersFullTime.baseSalary)
+ : undefined,
+ bonus:
+ res.offersFullTime.bonus != null
+ ? convertMoneyToString(res.offersFullTime.bonus)
+ : undefined,
companyName: res.company.name,
id: res.offersFullTime.id,
jobLevel: res.offersFullTime.level,
- jobTitle: res.offersFullTime.title,
+ jobTitle: getLabelForJobTitleType(
+ res.offersFullTime.title as JobTitleType,
+ ),
location: res.location,
negotiationStrategy: res.negotiationStrategy,
otherComment: res.comments,
receivedMonth: formatDate(res.monthYearReceived),
- stocks: convertMoneyToString(res.offersFullTime.stocks),
+ stocks:
+ res.offersFullTime.stocks != null
+ ? convertMoneyToString(res.offersFullTime.stocks)
+ : undefined,
totalCompensation: convertMoneyToString(
res.offersFullTime.totalCompensation,
),
@@ -77,7 +91,9 @@ export default function OfferProfile() {
const filteredOffer: OfferDisplayData = {
companyName: res.company.name,
id: res.offersIntern!.id,
- jobTitle: res.offersIntern!.title,
+ jobTitle: getLabelForJobTitleType(
+ res.offersIntern!.title as JobTitleType,
+ ),
location: res.location,
monthlySalary: convertMoneyToString(
res.offersIntern!.monthlySalary,
@@ -107,7 +123,9 @@ export default function OfferProfile() {
companyName: experience.company?.name,
duration: experience.durationInMonths,
jobLevel: experience.level,
- jobTitle: experience.title,
+ jobTitle: experience.title
+ ? getLabelForJobTitleType(experience.title as JobTitleType)
+ : null,
monthlySalary: experience.monthlySalary
? convertMoneyToString(experience.monthlySalary)
: null,
@@ -140,7 +158,7 @@ export default function OfferProfile() {
},
onSuccess: () => {
trpcContext.invalidateQueries(['offers.profile.listOne']);
- router.push('/offers');
+ router.push(HOME_URL);
showToast({
title: `Offers profile successfully deleted!`,
variant: 'success',
diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx
index 972ba6ee..5f625321 100644
--- a/apps/portal/src/pages/offers/test/createProfile.tsx
+++ b/apps/portal/src/pages/offers/test/createProfile.tsx
@@ -107,8 +107,7 @@ function Test() {
durationInMonths: 24,
jobType: 'FULLTIME',
level: 'Junior',
- specialization: 'Front End',
- title: 'Software Engineer',
+ title: 'software-engineer',
totalCompensation: {
currency: 'SGD',
value: 104100,
@@ -146,12 +145,11 @@ function Test() {
value: 2222,
},
level: 'Junior',
- specialization: 'Front End',
stocks: {
currency: 'SGD',
value: 0,
},
- title: 'Software Engineer',
+ title: 'software-engineer',
totalCompensation: {
currency: 'SGD',
value: 4444,
@@ -175,12 +173,11 @@ function Test() {
value: 20000,
},
level: 'Junior',
- specialization: 'Front End',
stocks: {
currency: 'SGD',
value: 100,
},
- title: 'Software Engineer',
+ title: 'software-engineer',
totalCompensation: {
currency: 'SGD',
value: 104100,
@@ -269,8 +266,7 @@ function Test() {
level: 'Junior',
monthlySalary: null,
monthlySalaryId: null,
- specialization: 'Front End',
- title: 'Software Engineer',
+ title: 'software-engineer',
totalCompensation: {
currency: 'SGD',
id: 'cl9i68fvc0005tthj7r1rhvb1',
@@ -335,14 +331,13 @@ function Test() {
bonusId: 'cl9i68fve000rtthjqo2ktljt',
id: 'cl9i68fve000otthjqk0g01k0',
level: 'EXPERT',
- specialization: 'FRONTEND',
stocks: {
currency: 'SGD',
id: 'cl9i68fvf000ttthjt2ode0cc',
value: -558038585,
},
stocksId: 'cl9i68fvf000ttthjt2ode0cc',
- title: 'Software Engineer',
+ title: 'software-engineer',
totalCompensation: {
currency: 'SGD',
id: 'cl9i68fvf000vtthjg90s48nj',
@@ -355,220 +350,8 @@ function Test() {
offersInternId: null,
profileId: 'cl9i68fv60000tthj8t3zkox0',
},
- // {
- // comments: '',
- // company: {
- // createdAt: new Date('2022-10-12T16:19:05.196Z'),
- // description:
- // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
- // id: 'cl9j4yawz0003utlp1uaa1t8o',
- // logoUrl: 'https://logo.clearbit.com/meta.com',
- // name: 'Meta',
- // slug: 'meta',
- // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
- // },
- // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
- // id: 'cl9i68fvf000ytthj0ltsqt1d',
- // jobType: 'FULLTIME',
- // location: 'Singapore, Singapore',
- // monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
- // negotiationStrategy: 'Leveraged having million offers',
- // offersFullTime: {
- // baseSalary: {
- // currency: 'SGD',
- // id: 'cl9i68fvf0010tthj0iym6woh',
- // value: 84000,
- // },
- // baseSalaryId: 'cl9i68fvf0010tthj0iym6woh',
- // bonus: {
- // currency: 'SGD',
- // id: 'cl9i68fvf0012tthjioltnspk',
- // value: 123456789,
- // },
- // bonusId: 'cl9i68fvf0012tthjioltnspk',
- // id: 'cl9i68fvf000ztthjcovbiehc',
- // level: 'Junior',
- // specialization: 'Front End',
- // stocks: {
- // currency: 'SGD',
- // id: 'cl9i68fvf0014tthjz2gff3hs',
- // value: 100,
- // },
- // stocksId: 'cl9i68fvf0014tthjz2gff3hs',
- // title: 'Software Engineer',
- // totalCompensation: {
- // currency: 'SGD',
- // id: 'cl9i68fvf0016tthjrtb7iuvj',
- // value: 104100,
- // },
- // totalCompensationId: 'cl9i68fvf0016tthjrtb7iuvj',
- // },
- // offersFullTimeId: 'cl9i68fvf000ztthjcovbiehc',
- // offersIntern: null,
- // offersInternId: null,
- // profileId: 'cl9i68fv60000tthj8t3zkox0',
- // },
- // {
- // comments: '',
- // company: {
- // createdAt: new Date('2022-10-12T16:19:05.196Z'),
- // description:
- // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
- // id: 'cl9j4yawz0003utlp1uaa1t8o',
- // logoUrl: 'https://logo.clearbit.com/meta.com',
- // name: 'Meta',
- // slug: 'meta',
- // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
- // },
- // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
- // id: 'cl96stky9003bw32gc3l955vr',
- // jobType: 'FULLTIME',
- // location: 'Singapore, Singapore',
- // monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
- // negotiationStrategy: 'LOst out having multiple offers',
- // offersFullTime: {
- // baseSalary: {
- // currency: 'SGD',
- // id: 'cl96stky9003dw32gcvqbijlo',
- // value: 1,
- // },
- // baseSalaryId: 'cl96stky9003dw32gcvqbijlo',
- // bonus: {
- // currency: 'SGD',
- // id: 'cl96stky9003fw32goc3zqxwr',
- // value: 0,
- // },
- // bonusId: 'cl96stky9003fw32goc3zqxwr',
- // id: 'cl96stky9003cw32g5v10izfu',
- // level: 'Senior',
- // specialization: 'Front End',
- // stocks: {
- // currency: 'SGD',
- // id: 'cl96stky9003hw32g1lbbkqqr',
- // value: 999999,
- // },
- // stocksId: 'cl96stky9003hw32g1lbbkqqr',
- // title: 'Software Engineer DOG',
- // totalCompensation: {
- // currency: 'SGD',
- // id: 'cl96stky9003jw32gzumcoi7v',
- // value: 999999,
- // },
- // totalCompensationId: 'cl96stky9003jw32gzumcoi7v',
- // },
- // offersFullTimeId: 'cl96stky9003cw32g5v10izfu',
- // offersIntern: null,
- // offersInternId: null,
- // profileId: 'cl96stky5002ew32gx2kale2x',
- // },
- // {
- // comments: 'this IS SO COOL',
- // company: {
- // createdAt: new Date('2022-10-12T16:19:05.196Z'),
- // description:
- // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
- // id: 'cl9j4yawz0003utlp1uaa1t8o',
- // logoUrl: 'https://logo.clearbit.com/meta.com',
- // name: 'Meta',
- // slug: 'meta',
- // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
- // },
- // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
- // id: 'cl976wf28000t7iyga4noyz7s',
- // jobType: 'FULLTIME',
- // location: 'Singapore, Singapore',
- // monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
- // negotiationStrategy: 'Charmed the guy with my face',
- // offersFullTime: {
- // baseSalary: {
- // currency: 'SGD',
- // id: 'cl976wf28000v7iygmk1b7qaq',
- // value: 1999999999,
- // },
- // baseSalaryId: 'cl976wf28000v7iygmk1b7qaq',
- // bonus: {
- // currency: 'SGD',
- // id: 'cl976wf28000x7iyg63w7kcli',
- // value: 1410065407,
- // },
- // bonusId: 'cl976wf28000x7iyg63w7kcli',
- // id: 'cl976wf28000u7iyg6euei8e9',
- // level: 'EXPERT',
- // specialization: 'FRONTEND',
- // stocks: {
- // currency: 'SGD',
- // id: 'cl976wf28000z7iyg9ivun6ap',
- // value: 111222333,
- // },
- // stocksId: 'cl976wf28000z7iyg9ivun6ap',
- // title: 'Software Engineer',
- // totalCompensation: {
- // currency: 'SGD',
- // id: 'cl976wf2800117iygmzsc0xit',
- // value: 55555555,
- // },
- // totalCompensationId: 'cl976wf2800117iygmzsc0xit',
- // },
- // offersFullTimeId: 'cl976wf28000u7iyg6euei8e9',
- // offersIntern: null,
- // offersInternId: null,
- // profileId: 'cl96stky5002ew32gx2kale2x',
- // },
- // {
- // comments: 'this rocks',
- // company: {
- // createdAt: new Date('2022-10-12T16:19:05.196Z'),
- // description:
- // 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
- // id: 'cl9j4yawz0003utlp1uaa1t8o',
- // logoUrl: 'https://logo.clearbit.com/meta.com',
- // name: 'Meta',
- // slug: 'meta',
- // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
- // },
- // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
- // id: 'cl96tbb3o0051w32gjrpaiiit',
- // jobType: 'FULLTIME',
- // location: 'Singapore, Singapore',
- // monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
- // negotiationStrategy: 'Charmed the guy with my face',
- // offersFullTime: {
- // baseSalary: {
- // currency: 'SGD',
- // id: 'cl96tbb3o0053w32gz11paaxu',
- // value: 1999999999,
- // },
- // baseSalaryId: 'cl96tbb3o0053w32gz11paaxu',
- // bonus: {
- // currency: 'SGD',
- // id: 'cl96tbb3o0055w32gpyqgz5hx',
- // value: 1410065407,
- // },
- // bonusId: 'cl96tbb3o0055w32gpyqgz5hx',
- // id: 'cl96tbb3o0052w32guguajzin',
- // level: 'EXPERT',
- // specialization: 'FRONTEND',
- // stocks: {
- // currency: 'SGD',
- // id: 'cl96tbb3o0057w32gu4nyxguf',
- // value: 500,
- // },
- // stocksId: 'cl96tbb3o0057w32gu4nyxguf',
- // title: 'Software Engineer',
- // totalCompensation: {
- // currency: 'SGD',
- // id: 'cl96tbb3o0059w32gm3iy1zk4',
- // value: 55555555,
- // },
- // totalCompensationId: 'cl96tbb3o0059w32gm3iy1zk4',
- // },
- // offersFullTimeId: 'cl96tbb3o0052w32guguajzin',
- // offersIntern: null,
- // offersInternId: null,
- // profileId: 'cl96stky5002ew32gx2kale2x',
- // },
],
- // ProfileName: 'ailing bryann stuart ziqing',
+
token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
userId: null,
});
diff --git a/apps/portal/src/pages/questions/browse.tsx b/apps/portal/src/pages/questions/browse.tsx
index 163d4842..cb895f27 100644
--- a/apps/portal/src/pages/questions/browse.tsx
+++ b/apps/portal/src/pages/questions/browse.tsx
@@ -5,24 +5,22 @@ import { useEffect, useMemo, useState } from 'react';
import { Bars3BottomLeftIcon } from '@heroicons/react/20/solid';
import { NoSymbolIcon } from '@heroicons/react/24/outline';
import type { QuestionsQuestionType } from '@prisma/client';
-import { Button, SlideOut, Typeahead } from '@tih/ui';
+import { Button, SlideOut } from '@tih/ui';
import QuestionOverviewCard from '~/components/questions/card/question/QuestionOverviewCard';
import ContributeQuestionCard from '~/components/questions/ContributeQuestionCard';
+import type { FilterOption } from '~/components/questions/filter/FilterSection';
import FilterSection from '~/components/questions/filter/FilterSection';
import QuestionSearchBar from '~/components/questions/QuestionSearchBar';
+import CompanyTypeahead from '~/components/questions/typeahead/CompanyTypeahead';
+import LocationTypeahead from '~/components/questions/typeahead/LocationTypeahead';
+import RoleTypeahead from '~/components/questions/typeahead/RoleTypeahead';
import type { QuestionAge } from '~/utils/questions/constants';
import { SORT_TYPES } from '~/utils/questions/constants';
import { SORT_ORDERS } from '~/utils/questions/constants';
import { APP_TITLE } from '~/utils/questions/constants';
-import { ROLES } from '~/utils/questions/constants';
-import {
- COMPANIES,
- LOCATIONS,
- QUESTION_AGES,
- QUESTION_TYPES,
-} from '~/utils/questions/constants';
+import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
import {
useSearchParam,
@@ -148,12 +146,18 @@ export default function QuestionsBrowsePage() {
: undefined;
}, [selectedQuestionAge]);
- const { data: questions } = trpc.useQuery(
+ const {
+ data: questionsQueryData,
+ fetchNextPage,
+ hasNextPage,
+ isFetchingNextPage,
+ } = trpc.useInfiniteQuery(
[
'questions.questions.getQuestionsByFilter',
{
companyNames: selectedCompanies,
endDate: today,
+ limit: 10,
locations: selectedLocations,
questionTypes: selectedQuestionTypes,
roles: selectedRoles,
@@ -163,10 +167,21 @@ export default function QuestionsBrowsePage() {
},
],
{
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
keepPreviousData: true,
},
);
+ const questionCount = useMemo(() => {
+ if (!questionsQueryData) {
+ return undefined;
+ }
+ return questionsQueryData.pages.reduce(
+ (acc, page) => acc + page.data.length,
+ 0,
+ );
+ }, [questionsQueryData]);
+
const utils = trpc.useContext();
const { mutate: createQuestion } = trpc.useMutation(
'questions.questions.create',
@@ -180,12 +195,17 @@ export default function QuestionsBrowsePage() {
const [loaded, setLoaded] = useState(false);
const [filterDrawerOpen, setFilterDrawerOpen] = useState(false);
- const companyFilterOptions = useMemo(() => {
- return COMPANIES.map((company) => ({
- ...company,
- checked: selectedCompanies.includes(company.value),
- }));
- }, [selectedCompanies]);
+ const [selectedCompanyOptions, setSelectedCompanyOptions] = useState<
+ Array
+ >([]);
+
+ const [selectedRoleOptions, setSelectedRoleOptions] = useState<
+ Array
+ >([]);
+
+ const [selectedLocationOptions, setSelectedLocationOptions] = useState<
+ Array
+ >([]);
const questionTypeFilterOptions = useMemo(() => {
return QUESTION_TYPES.map((questionType) => ({
@@ -201,20 +221,6 @@ export default function QuestionsBrowsePage() {
}));
}, [selectedQuestionAge]);
- const roleFilterOptions = useMemo(() => {
- return ROLES.map((role) => ({
- ...role,
- checked: selectedRoles.includes(role.value),
- }));
- }, [selectedRoles]);
-
- const locationFilterOptions = useMemo(() => {
- return LOCATIONS.map((location) => ({
- ...location,
- checked: selectedLocations.includes(location.value),
- }));
- }, [selectedLocations]);
-
const areSearchOptionsInitialized = useMemo(() => {
return (
areCompaniesInitialized &&
@@ -287,35 +293,89 @@ export default function QuestionsBrowsePage() {
setSelectedQuestionAge('all');
setSelectedRoles([]);
setSelectedLocations([]);
+ setSelectedCompanyOptions([]);
+ setSelectedRoleOptions([]);
+ setSelectedLocationOptions([]);
}}
/>
(
- (
+ {
+ return !selectedCompanyOptions.some((selectedOption) => {
+ return selectedOption.value === option.value;
+ });
+ }}
isLabelHidden={true}
- label="Companies"
- options={options}
placeholder="Search companies"
- // eslint-disable-next-line @typescript-eslint/no-empty-function
- onQueryChange={() => {}}
- onSelect={({ value }) => {
- onOptionChange(value, true);
+ onSelect={(option) => {
+ onOptionChange({
+ ...option,
+ checked: true,
+ });
}}
/>
)}
- onOptionChange={(optionValue, checked) => {
- if (checked) {
- setSelectedCompanies([...selectedCompanies, optionValue]);
+ onOptionChange={(option) => {
+ if (option.checked) {
+ setSelectedCompanies([...selectedCompanies, option.label]);
+ setSelectedCompanyOptions((prevOptions) => [
+ ...prevOptions,
+ { ...option, checked: true },
+ ]);
} else {
setSelectedCompanies(
- selectedCompanies.filter((company) => company !== optionValue),
+ selectedCompanies.filter((company) => company !== option.label),
+ );
+ setSelectedCompanyOptions((prevOptions) =>
+ prevOptions.filter(
+ (prevOption) => prevOption.label !== option.label,
+ ),
+ );
+ }
+ }}
+ />
+ (
+ {
+ return !selectedRoleOptions.some((selectedOption) => {
+ return selectedOption.value === option.value;
+ });
+ }}
+ isLabelHidden={true}
+ placeholder="Search roles"
+ onSelect={(option) => {
+ onOptionChange({
+ ...option,
+ checked: true,
+ });
+ }}
+ />
+ )}
+ onOptionChange={(option) => {
+ if (option.checked) {
+ setSelectedRoles([...selectedRoles, option.value]);
+ setSelectedRoleOptions((prevOptions) => [
+ ...prevOptions,
+ { ...option, checked: true },
+ ]);
+ } else {
+ setSelectedRoles(
+ selectedCompanies.filter((role) => role !== option.value),
+ );
+ setSelectedRoleOptions((prevOptions) =>
+ prevOptions.filter(
+ (prevOption) => prevOption.value !== option.value,
+ ),
);
}
}}
@@ -324,13 +384,13 @@ export default function QuestionsBrowsePage() {
label="Question types"
options={questionTypeFilterOptions}
showAll={true}
- onOptionChange={(optionValue, checked) => {
- if (checked) {
- setSelectedQuestionTypes([...selectedQuestionTypes, optionValue]);
+ onOptionChange={(option) => {
+ if (option.checked) {
+ setSelectedQuestionTypes([...selectedQuestionTypes, option.value]);
} else {
setSelectedQuestionTypes(
selectedQuestionTypes.filter(
- (questionType) => questionType !== optionValue,
+ (questionType) => questionType !== option.value,
),
);
}
@@ -341,68 +401,47 @@ export default function QuestionsBrowsePage() {
label="Question age"
options={questionAgeFilterOptions}
showAll={true}
- onOptionChange={(optionValue) => {
- setSelectedQuestionAge(optionValue);
+ onOptionChange={({ value }) => {
+ setSelectedQuestionAge(value);
}}
/>
(
- (
+ {}}
- onSelect={({ value }) => {
- onOptionChange(value, true);
+ clearOnSelect={true}
+ filterOption={(option) => {
+ return !selectedLocationOptions.some((selectedOption) => {
+ return selectedOption.value === option.value;
+ });
}}
- />
- )}
- onOptionChange={(optionValue, checked) => {
- if (checked) {
- setSelectedRoles([...selectedRoles, optionValue]);
- } else {
- setSelectedRoles(
- selectedRoles.filter((role) => role !== optionValue),
- );
- }
- }}
- />
- (
- {}}
- onSelect={({ value }) => {
- onOptionChange(value, true);
+ onSelect={(option) => {
+ onOptionChange({
+ ...option,
+ checked: true,
+ });
}}
/>
)}
- onOptionChange={(optionValue, checked) => {
- if (checked) {
- setSelectedLocations([...selectedLocations, optionValue]);
+ onOptionChange={(option) => {
+ if (option.checked) {
+ setSelectedLocations([...selectedLocations, option.value]);
+ setSelectedLocationOptions((prevOptions) => [
+ ...prevOptions,
+ { ...option, checked: true },
+ ]);
} else {
setSelectedLocations(
- selectedLocations.filter((location) => location !== optionValue),
+ selectedLocations.filter((role) => role !== option.value),
+ );
+ setSelectedLocationOptions((prevOptions) =>
+ prevOptions.filter(
+ (prevOption) => prevOption.value !== option.value,
+ ),
);
}
}}
@@ -443,29 +482,50 @@ export default function QuestionsBrowsePage() {
onSortOrderChange={setSortOrder}
onSortTypeChange={setSortType}
/>
-
- {(questions ?? []).map((question) => (
-
- ))}
- {questions?.length === 0 && (
+
+ {(questionsQueryData?.pages ?? []).flatMap(
+ ({ data: questions }) =>
+ questions.map((question) => (
+
+ )),
+ )}
+
{
+ fetchNextPage();
+ }}
+ />
+ {questionCount === 0 && (
Nothing found.
diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx
index bc703d4b..38c7cf31 100644
--- a/apps/portal/src/pages/resumes/[resumeId].tsx
+++ b/apps/portal/src/pages/resumes/[resumeId].tsx
@@ -100,7 +100,7 @@ export default function ResumeReviewPage() {
}
return (
{detailsQuery.data.title}
-
+
{userIsOwner && (
-
+
)}
diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx
index c851cafa..7901056b 100644
--- a/apps/portal/src/pages/resumes/submit.tsx
+++ b/apps/portal/src/pages/resumes/submit.tsx
@@ -86,10 +86,16 @@ export default function SubmitResumeForm({
setValue,
reset,
watch,
+ clearErrors,
formState: { errors, isDirty, dirtyFields },
} = useForm({
defaultValues: {
+ additionalInfo: '',
+ experience: '',
isChecked: false,
+ location: '',
+ role: '',
+ title: '',
...initFormDetails,
},
});
@@ -296,7 +302,7 @@ export default function SubmitResumeForm({
options={ROLES}
placeholder=" "
required={true}
- onChange={(val) => setValue('role', val)}
+ onChange={(val) => onValueChange('role', val)}
/>
setValue('experience', val)}
+ onChange={(val) => onValueChange('experience', val)}
/>
setValue('location', val)}
+ onChange={(val) => onValueChange('location', val)}
/>
{/* Upload resume form */}
{isNewForm && (
@@ -335,6 +341,16 @@ export default function SubmitResumeForm({
: 'border-slate-300',
'flex cursor-pointer justify-center rounded-md border-2 border-dashed bg-slate-100 py-4',
)}>
+
{resumeFile == null ? (
@@ -345,29 +361,15 @@ export default function SubmitResumeForm({
{resumeFile.name}
)}
-
-
- Drop file here
- or
-
- {resumeFile == null
- ? 'Select file'
- : 'Replace file'}
-
-
-
-
+
+ Drop file here
+ or
+
+ {resumeFile == null ? 'Select file' : 'Replace file'}
+
+
PDF up to {FILE_SIZE_LIMIT_MB}MB
@@ -394,8 +396,18 @@ export default function SubmitResumeForm({
setValue('isChecked', val)}
+ onChange={(val) => {
+ if (val) {
+ clearErrors('isChecked');
+ }
+ setValue('isChecked', val);
+ }}
/>
>
)}
diff --git a/apps/portal/src/server/router/offers/offers-analysis-router.ts b/apps/portal/src/server/router/offers/offers-analysis-router.ts
index 751e6cd2..9ee081cd 100644
--- a/apps/portal/src/server/router/offers/offers-analysis-router.ts
+++ b/apps/portal/src/server/router/offers/offers-analysis-router.ts
@@ -19,9 +19,9 @@ const searchOfferPercentile = (
company: Company;
offersFullTime:
| (OffersFullTime & {
- baseSalary: OffersCurrency;
- bonus: OffersCurrency;
- stocks: OffersCurrency;
+ baseSalary: OffersCurrency | null;
+ bonus: OffersCurrency | null;
+ stocks: OffersCurrency | null;
totalCompensation: OffersCurrency;
})
| null;
diff --git a/apps/portal/src/server/router/offers/offers-profile-router.ts b/apps/portal/src/server/router/offers/offers-profile-router.ts
index 9cac7feb..bf86613a 100644
--- a/apps/portal/src/server/router/offers/offers-profile-router.ts
+++ b/apps/portal/src/server/router/offers/offers-profile-router.ts
@@ -10,7 +10,10 @@ import {
} from '~/mappers/offers-mappers';
import { baseCurrencyString } from '~/utils/offers/currency';
import { convert } from '~/utils/offers/currency/currencyExchange';
-import { generateRandomName, generateRandomStringForToken } from '~/utils/offers/randomGenerator';
+import {
+ generateRandomName,
+ generateRandomStringForToken,
+} from '~/utils/offers/randomGenerator';
import { createValidationRegex } from '~/utils/offers/zodRegex';
import { createRouter } from '../context';
@@ -48,7 +51,6 @@ const offer = z.object({
bonusId: z.string().nullish(),
id: z.string().optional(),
level: z.string().nullish(),
- specialization: z.string(),
stocks: valuation.nullish(),
stocksId: z.string().nullish(),
title: z.string(),
@@ -62,7 +64,6 @@ const offer = z.object({
id: z.string().optional(),
internshipCycle: z.string().nullish(),
monthlySalary: valuation.nullish(),
- specialization: z.string(),
startYear: z.number().nullish(),
title: z.string(),
totalCompensation: valuation.nullish(), // Full time
@@ -86,7 +87,6 @@ const experience = z.object({
location: z.string().nullish(),
monthlySalary: valuation.nullish(),
monthlySalaryId: z.string().nullish(),
- specialization: z.string().nullish(),
title: z.string().nullish(),
totalCompensation: valuation.nullish(),
totalCompensationId: z.string().nullish(),
@@ -300,7 +300,6 @@ export const offersProfileRouter = createRouter()
durationInMonths: x.durationInMonths,
jobType: x.jobType,
level: x.level,
- specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
@@ -321,7 +320,6 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType,
level: x.level,
location: x.location,
- specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
@@ -363,7 +361,6 @@ export const offersProfileRouter = createRouter()
value: x.monthlySalary.value,
},
},
- specialization: x.specialization,
title: x.title,
};
}
@@ -382,7 +379,6 @@ export const offersProfileRouter = createRouter()
value: x.monthlySalary.value,
},
},
- specialization: x.specialization,
title: x.title,
};
}
@@ -442,7 +438,6 @@ export const offersProfileRouter = createRouter()
value: x.offersIntern.monthlySalary.value,
},
},
- specialization: x.offersIntern.specialization,
startYear: x.offersIntern.startYear,
title: x.offersIntern.title,
},
@@ -452,17 +447,10 @@ export const offersProfileRouter = createRouter()
if (
x.jobType === JobType.FULLTIME &&
x.offersFullTime &&
- x.offersFullTime.baseSalary?.currency != null &&
- x.offersFullTime.baseSalary?.value != null &&
- x.offersFullTime.bonus?.currency != null &&
- x.offersFullTime.bonus?.value != null &&
- x.offersFullTime.stocks?.currency != null &&
- x.offersFullTime.stocks?.value != null &&
x.offersFullTime.totalCompensation?.currency != null &&
x.offersFullTime.totalCompensation?.value != null &&
x.offersFullTime.level != null &&
- x.offersFullTime.title != null &&
- x.offersFullTime.specialization != null
+ x.offersFullTime.title != null
) {
return {
comments: x.comments,
@@ -477,44 +465,53 @@ export const offersProfileRouter = createRouter()
negotiationStrategy: x.negotiationStrategy,
offersFullTime: {
create: {
- baseSalary: {
- create: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- x.offersFullTime.baseSalary.value,
- x.offersFullTime.baseSalary.currency,
- baseCurrencyString,
- ),
- currency: x.offersFullTime.baseSalary.currency,
- value: x.offersFullTime.baseSalary.value,
- },
- },
- bonus: {
- create: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- x.offersFullTime.bonus.value,
- x.offersFullTime.bonus.currency,
- baseCurrencyString,
- ),
- currency: x.offersFullTime.bonus.currency,
- value: x.offersFullTime.bonus.value,
- },
- },
+ baseSalary:
+ x.offersFullTime?.baseSalary != null
+ ? {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ x.offersFullTime.baseSalary.value,
+ x.offersFullTime.baseSalary.currency,
+ baseCurrencyString,
+ ),
+ currency:
+ x.offersFullTime.baseSalary.currency,
+ value: x.offersFullTime.baseSalary.value,
+ },
+ }
+ : undefined,
+ bonus:
+ x.offersFullTime?.bonus != null
+ ? {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ x.offersFullTime.bonus.value,
+ x.offersFullTime.bonus.currency,
+ baseCurrencyString,
+ ),
+ currency: x.offersFullTime.bonus.currency,
+ value: x.offersFullTime.bonus.value,
+ },
+ }
+ : undefined,
level: x.offersFullTime.level,
- specialization: x.offersFullTime.specialization,
- stocks: {
- create: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- x.offersFullTime.stocks.value,
- x.offersFullTime.stocks.currency,
- baseCurrencyString,
- ),
- currency: x.offersFullTime.stocks.currency,
- value: x.offersFullTime.stocks.value,
- },
- },
+ stocks:
+ x.offersFullTime?.stocks != null
+ ? {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ x.offersFullTime.stocks.value,
+ x.offersFullTime.stocks.currency,
+ baseCurrencyString,
+ ),
+ currency: x.offersFullTime.stocks.currency,
+ value: x.offersFullTime.stocks.value,
+ },
+ }
+ : undefined,
title: x.offersFullTime.title,
totalCompensation: {
create: {
@@ -714,7 +711,6 @@ export const offersProfileRouter = createRouter()
companyId: exp.companyId, // TODO: check if can change with connect or whether there is a difference
durationInMonths: exp.durationInMonths,
level: exp.level,
- specialization: exp.specialization,
},
where: {
id: exp.id,
@@ -821,7 +817,6 @@ export const offersProfileRouter = createRouter()
jobType: exp.jobType,
level: exp.level,
location: exp.location,
- specialization: exp.specialization,
title: exp.title,
totalCompensation: {
create: {
@@ -851,7 +846,6 @@ export const offersProfileRouter = createRouter()
jobType: exp.jobType,
level: exp.level,
location: exp.location,
- specialization: exp.specialization,
title: exp.title,
totalCompensation: {
create: {
@@ -887,7 +881,6 @@ export const offersProfileRouter = createRouter()
jobType: exp.jobType,
level: exp.level,
location: exp.location,
- specialization: exp.specialization,
title: exp.title,
},
},
@@ -905,7 +898,6 @@ export const offersProfileRouter = createRouter()
jobType: exp.jobType,
level: exp.level,
location: exp.location,
- specialization: exp.specialization,
title: exp.title,
},
},
@@ -945,7 +937,6 @@ export const offersProfileRouter = createRouter()
value: exp.monthlySalary.value,
},
},
- specialization: exp.specialization,
title: exp.title,
},
},
@@ -974,7 +965,6 @@ export const offersProfileRouter = createRouter()
value: exp.monthlySalary.value,
},
},
- specialization: exp.specialization,
title: exp.title,
},
},
@@ -997,7 +987,6 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
- specialization: exp.specialization,
title: exp.title,
},
},
@@ -1014,7 +1003,6 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
- specialization: exp.specialization,
title: exp.title,
},
},
@@ -1121,7 +1109,6 @@ export const offersProfileRouter = createRouter()
data: {
internshipCycle:
offerToUpdate.offersIntern.internshipCycle ?? undefined,
- specialization: offerToUpdate.offersIntern.specialization,
startYear: offerToUpdate.offersIntern.startYear ?? undefined,
title: offerToUpdate.offersIntern.title,
},
@@ -1150,7 +1137,6 @@ export const offersProfileRouter = createRouter()
await ctx.prisma.offersFullTime.update({
data: {
level: offerToUpdate.offersFullTime.level ?? undefined,
- specialization: offerToUpdate.offersFullTime.specialization,
title: offerToUpdate.offersFullTime.title,
},
where: {
@@ -1174,7 +1160,7 @@ export const offersProfileRouter = createRouter()
},
});
}
- if (offerToUpdate.offersFullTime.bonus) {
+ if (offerToUpdate.offersFullTime.bonus != null) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
@@ -1191,7 +1177,7 @@ export const offersProfileRouter = createRouter()
},
});
}
- if (offerToUpdate.offersFullTime.stocks) {
+ if (offerToUpdate.offersFullTime.stocks != null) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
@@ -1269,8 +1255,6 @@ export const offersProfileRouter = createRouter()
offerToUpdate.offersIntern.monthlySalary.value,
},
},
- specialization:
- offerToUpdate.offersIntern.specialization,
startYear: offerToUpdate.offersIntern.startYear,
title: offerToUpdate.offersIntern.title,
},
@@ -1286,12 +1270,6 @@ export const offersProfileRouter = createRouter()
if (
offerToUpdate.jobType === JobType.FULLTIME &&
offerToUpdate.offersFullTime &&
- offerToUpdate.offersFullTime.baseSalary?.currency != null &&
- offerToUpdate.offersFullTime.baseSalary?.value != null &&
- offerToUpdate.offersFullTime.bonus?.currency != null &&
- offerToUpdate.offersFullTime.bonus?.value != null &&
- offerToUpdate.offersFullTime.stocks?.currency != null &&
- offerToUpdate.offersFullTime.stocks?.value != null &&
offerToUpdate.offersFullTime.totalCompensation?.currency !=
null &&
offerToUpdate.offersFullTime.totalCompensation?.value != null &&
@@ -1313,51 +1291,66 @@ export const offersProfileRouter = createRouter()
negotiationStrategy: offerToUpdate.negotiationStrategy,
offersFullTime: {
create: {
- baseSalary: {
- create: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- offerToUpdate.offersFullTime.baseSalary.value,
- offerToUpdate.offersFullTime.baseSalary
- .currency,
- baseCurrencyString,
- ),
- currency:
- offerToUpdate.offersFullTime.baseSalary
- .currency,
- value:
- offerToUpdate.offersFullTime.baseSalary.value,
- },
- },
- bonus: {
- create: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- offerToUpdate.offersFullTime.bonus.value,
- offerToUpdate.offersFullTime.bonus.currency,
- baseCurrencyString,
- ),
- currency:
- offerToUpdate.offersFullTime.bonus.currency,
- value: offerToUpdate.offersFullTime.bonus.value,
- },
- },
+ baseSalary:
+ offerToUpdate.offersFullTime?.baseSalary != null
+ ? {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.baseSalary
+ .value,
+ offerToUpdate.offersFullTime.baseSalary
+ .currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersFullTime.baseSalary
+ .currency,
+ value:
+ offerToUpdate.offersFullTime.baseSalary
+ .value,
+ },
+ }
+ : undefined,
+ bonus:
+ offerToUpdate.offersFullTime?.bonus != null
+ ? {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.bonus.value,
+ offerToUpdate.offersFullTime.bonus
+ .currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersFullTime.bonus
+ .currency,
+ value:
+ offerToUpdate.offersFullTime.bonus.value,
+ },
+ }
+ : undefined,
level: offerToUpdate.offersFullTime.level,
- specialization:
- offerToUpdate.offersFullTime.specialization,
- stocks: {
- create: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- offerToUpdate.offersFullTime.stocks.value,
- offerToUpdate.offersFullTime.stocks.currency,
- baseCurrencyString,
- ),
- currency:
- offerToUpdate.offersFullTime.stocks.currency,
- value: offerToUpdate.offersFullTime.stocks.value,
- },
- },
+ stocks:
+ offerToUpdate.offersFullTime?.stocks != null
+ ? {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.stocks.value,
+ offerToUpdate.offersFullTime.stocks
+ .currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersFullTime.stocks
+ .currency,
+ value:
+ offerToUpdate.offersFullTime.stocks.value,
+ },
+ }
+ : undefined,
title: offerToUpdate.offersFullTime.title,
totalCompensation: {
create: {
diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts
index b333fa66..84756179 100644
--- a/apps/portal/src/server/router/offers/offers.ts
+++ b/apps/portal/src/server/router/offers/offers.ts
@@ -317,27 +317,36 @@ export const offersRouter = createRouter().query('list', {
offer.offersFullTime.totalCompensation.updatedAt,
);
offer.offersFullTime.totalCompensation.currency = currency;
- offer.offersFullTime.baseSalary.value = await convertWithDate(
- offer.offersFullTime.baseSalary.value,
- offer.offersFullTime.baseSalary.currency,
- currency,
- offer.offersFullTime.baseSalary.updatedAt,
- );
- offer.offersFullTime.baseSalary.currency = currency;
- offer.offersFullTime.stocks.value = await convertWithDate(
- offer.offersFullTime.stocks.value,
- offer.offersFullTime.stocks.currency,
- currency,
- offer.offersFullTime.stocks.updatedAt,
- );
- offer.offersFullTime.stocks.currency = currency;
- offer.offersFullTime.bonus.value = await convertWithDate(
- offer.offersFullTime.bonus.value,
- offer.offersFullTime.bonus.currency,
- currency,
- offer.offersFullTime.bonus.updatedAt,
- );
- offer.offersFullTime.bonus.currency = currency;
+
+ if (offer.offersFullTime?.baseSalary != null) {
+ offer.offersFullTime.baseSalary.value = await convertWithDate(
+ offer.offersFullTime.baseSalary.value,
+ offer.offersFullTime.baseSalary.currency,
+ currency,
+ offer.offersFullTime.baseSalary.updatedAt,
+ );
+ offer.offersFullTime.baseSalary.currency = currency;
+ }
+
+ if (offer.offersFullTime?.stocks != null) {
+ offer.offersFullTime.stocks.value = await convertWithDate(
+ offer.offersFullTime.stocks.value,
+ offer.offersFullTime.stocks.currency,
+ currency,
+ offer.offersFullTime.stocks.updatedAt,
+ );
+ offer.offersFullTime.stocks.currency = currency;
+ }
+
+ if (offer.offersFullTime?.bonus != null) {
+ offer.offersFullTime.bonus.value = await convertWithDate(
+ offer.offersFullTime.bonus.value,
+ offer.offersFullTime.bonus.currency,
+ currency,
+ offer.offersFullTime.bonus.updatedAt,
+ );
+ offer.offersFullTime.bonus.currency = currency;
+ }
} else if (offer.offersIntern?.monthlySalary != null) {
offer.offersIntern.monthlySalary.value = await convertWithDate(
offer.offersIntern.monthlySalary.value,
diff --git a/apps/portal/src/server/router/questions-answer-comment-router.ts b/apps/portal/src/server/router/questions-answer-comment-router.ts
index 75977b41..63a9ede4 100644
--- a/apps/portal/src/server/router/questions-answer-comment-router.ts
+++ b/apps/portal/src/server/router/questions-answer-comment-router.ts
@@ -166,13 +166,29 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
const { answerCommentId, vote } = input;
- return await ctx.prisma.questionsAnswerCommentVote.create({
- data: {
- answerCommentId,
- userId,
- vote,
- },
- });
+ const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
+
+ const [answerCommentVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsAnswerCommentVote.create({
+ data: {
+ answerCommentId,
+ userId,
+ vote,
+ },
+ }),
+ ctx.prisma.questionsAnswerComment.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: answerCommentId,
+ },
+ }),
+ ]);
+
+ return answerCommentVote;
},
})
.mutation('updateVote', {
@@ -198,14 +214,30 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
});
}
- return await ctx.prisma.questionsAnswerCommentVote.update({
- data: {
- vote,
- },
- where: {
- id,
- },
- });
+ const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
+
+ const [answerCommentVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsAnswerCommentVote.update({
+ data: {
+ vote,
+ },
+ where: {
+ id,
+ },
+ }),
+ ctx.prisma.questionsAnswerComment.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: voteToUpdate.answerCommentId,
+ },
+ }),
+ ]);
+
+ return answerCommentVote;
},
})
.mutation('deleteVote', {
@@ -229,10 +261,26 @@ export const questionsAnswerCommentRouter = createProtectedRouter()
});
}
- return await ctx.prisma.questionsAnswerCommentVote.delete({
- where: {
- id: input.id,
- },
- });
+ const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
+
+ const [answerCommentVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsAnswerCommentVote.delete({
+ where: {
+ id: input.id,
+ },
+ }),
+ ctx.prisma.questionsAnswerComment.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: voteToDelete.answerCommentId,
+ },
+ }),
+ ]);
+ return answerCommentVote;
+
},
});
diff --git a/apps/portal/src/server/router/questions-answer-router.ts b/apps/portal/src/server/router/questions-answer-router.ts
index 5d386854..e2318ba7 100644
--- a/apps/portal/src/server/router/questions-answer-router.ts
+++ b/apps/portal/src/server/router/questions-answer-router.ts
@@ -229,13 +229,28 @@ export const questionsAnswerRouter = createProtectedRouter()
const { answerId, vote } = input;
- return await ctx.prisma.questionsAnswerVote.create({
- data: {
- answerId,
- userId,
- vote,
- },
- });
+ const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
+
+ const [answerVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsAnswerVote.create({
+ data: {
+ answerId,
+ userId,
+ vote,
+ },
+ }),
+ ctx.prisma.questionsAnswer.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: answerId,
+ },
+ }),
+ ]);
+ return answerVote;
},
})
.mutation('updateVote', {
@@ -260,14 +275,30 @@ export const questionsAnswerRouter = createProtectedRouter()
});
}
- return await ctx.prisma.questionsAnswerVote.update({
- data: {
- vote,
- },
- where: {
- id,
- },
- });
+ const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
+
+ const [questionsAnswerVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsAnswerVote.update({
+ data: {
+ vote,
+ },
+ where: {
+ id,
+ },
+ }),
+ ctx.prisma.questionsAnswer.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: voteToUpdate.answerId,
+ },
+ }),
+ ]);
+
+ return questionsAnswerVote;
},
})
.mutation('deleteVote', {
@@ -290,10 +321,26 @@ export const questionsAnswerRouter = createProtectedRouter()
});
}
- return await ctx.prisma.questionsAnswerVote.delete({
- where: {
- id: input.id,
- },
- });
+ const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
+
+ const [questionsAnswerVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsAnswerVote.delete({
+ where: {
+ id: input.id,
+ },
+ }),
+ ctx.prisma.questionsAnswer.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: voteToDelete.answerId,
+ },
+ }),
+ ]);
+ return questionsAnswerVote;
+
},
});
diff --git a/apps/portal/src/server/router/questions-question-comment-router.ts b/apps/portal/src/server/router/questions-question-comment-router.ts
index e2f786f9..28cf3b9d 100644
--- a/apps/portal/src/server/router/questions-question-comment-router.ts
+++ b/apps/portal/src/server/router/questions-question-comment-router.ts
@@ -166,13 +166,28 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
const userId = ctx.session?.user?.id;
const { questionCommentId, vote } = input;
- return await ctx.prisma.questionsQuestionCommentVote.create({
- data: {
- questionCommentId,
- userId,
- vote,
- },
- });
+ const incrementValue: number = vote === Vote.UPVOTE ? 1 : -1;
+
+ const [ questionCommentVote ] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsQuestionCommentVote.create({
+ data: {
+ questionCommentId,
+ userId,
+ vote,
+ },
+ }),
+ ctx.prisma.questionsQuestionComment.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: questionCommentId,
+ },
+ }),
+ ]);
+ return questionCommentVote;
},
})
.mutation('updateVote', {
@@ -198,14 +213,30 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
});
}
- return await ctx.prisma.questionsQuestionCommentVote.update({
- data: {
- vote,
- },
- where: {
- id,
- },
- });
+ const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
+
+ const [questionCommentVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsQuestionCommentVote.update({
+ data: {
+ vote,
+ },
+ where: {
+ id,
+ },
+ }),
+ ctx.prisma.questionsQuestionComment.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: voteToUpdate.questionCommentId,
+ },
+ }),
+ ]);
+
+ return questionCommentVote;
},
})
.mutation('deleteVote', {
@@ -229,10 +260,25 @@ export const questionsQuestionCommentRouter = createProtectedRouter()
});
}
- return await ctx.prisma.questionsQuestionCommentVote.delete({
- where: {
- id: input.id,
- },
- });
+ const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
+
+ const [questionCommentVote] = await ctx.prisma.$transaction([
+ ctx.prisma.questionsQuestionCommentVote.delete({
+ where: {
+ id: input.id,
+ },
+ }),
+ ctx.prisma.questionsQuestionComment.update({
+ data: {
+ upvotes: {
+ increment: incrementValue,
+ },
+ },
+ where: {
+ id: voteToDelete.questionCommentId,
+ },
+ }),
+ ]);
+ return questionCommentVote;
},
});
diff --git a/apps/portal/src/server/router/questions-question-encounter-router.ts b/apps/portal/src/server/router/questions-question-encounter-router.ts
index 1f328dc6..8fa4a0e4 100644
--- a/apps/portal/src/server/router/questions-question-encounter-router.ts
+++ b/apps/portal/src/server/router/questions-question-encounter-router.ts
@@ -25,9 +25,13 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
const locationCounts: Record = {};
const roleCounts: Record = {};
+ let latestSeenAt = questionEncountersData[0].seenAt;
+
for (let i = 0; i < questionEncountersData.length; i++) {
const encounter = questionEncountersData[i];
+ latestSeenAt = latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
+
if (!(encounter.company!.name in companyCounts)) {
companyCounts[encounter.company!.name] = 1;
}
@@ -46,6 +50,7 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
const questionEncounter: AggregatedQuestionEncounter = {
companyCounts,
+ latestSeenAt,
locationCounts,
roleCounts,
};
@@ -72,7 +77,6 @@ export const questionsQuestionEncounterRouter = createProtectedRouter()
},
})
.mutation('update', {
- //
input: z.object({
companyId: z.string().optional(),
id: z.string(),
diff --git a/apps/portal/src/server/router/questions-question-router.ts b/apps/portal/src/server/router/questions-question-router.ts
index 3768b852..3ea33bce 100644
--- a/apps/portal/src/server/router/questions-question-router.ts
+++ b/apps/portal/src/server/router/questions-question-router.ts
@@ -11,9 +11,16 @@ export const questionsQuestionRouter = createProtectedRouter()
.query('getQuestionsByFilter', {
input: z.object({
companyNames: z.string().array(),
+ cursor: z
+ .object({
+ idCursor: z.string().optional(),
+ lastSeenCursor: z.date().nullish().optional(),
+ upvoteCursor: z.number().optional(),
+ })
+ .nullish(),
endDate: z.date().default(new Date()),
+ limit: z.number().min(1).default(50),
locations: z.string().array(),
- pageSize: z.number().default(50),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
sortOrder: z.nativeEnum(SortOrder),
@@ -21,16 +28,34 @@ export const questionsQuestionRouter = createProtectedRouter()
startDate: z.date().optional(),
}),
async resolve({ ctx, input }) {
+ const { cursor } = input;
+
const sortCondition =
input.sortType === SortType.TOP
- ? {
- upvotes: input.sortOrder,
- }
- : {
- lastSeenAt: input.sortOrder,
- };
+ ? [
+ {
+ upvotes: input.sortOrder,
+ },
+ {
+ id: input.sortOrder,
+ },
+ ]
+ : [
+ {
+ lastSeenAt: input.sortOrder,
+ },
+ {
+ id: input.sortOrder,
+ },
+ ];
const questionsData = await ctx.prisma.questionsQuestion.findMany({
+ cursor:
+ cursor !== undefined
+ ? {
+ id: cursor ? cursor!.idCursor : undefined,
+ }
+ : undefined,
include: {
_count: {
select: {
@@ -53,9 +78,8 @@ export const questionsQuestionRouter = createProtectedRouter()
},
votes: true,
},
- orderBy: {
- ...sortCondition,
- },
+ orderBy: sortCondition,
+ take: input.limit + 1,
where: {
...(input.questionTypes.length > 0
? {
@@ -98,7 +122,7 @@ export const questionsQuestionRouter = createProtectedRouter()
},
});
- return questionsData.map((data) => {
+ const processedQuestionsData = questionsData.map((data) => {
const votes: number = data.votes.reduce(
(previousValue: number, currentValue) => {
let result: number = previousValue;
@@ -116,23 +140,78 @@ export const questionsQuestionRouter = createProtectedRouter()
0,
);
+ const companyCounts: Record = {};
+ const locationCounts: Record = {};
+ const roleCounts: Record = {};
+
+ let latestSeenAt = data.encounters[0].seenAt;
+
+ for (let i = 0; i < data.encounters.length; i++) {
+ const encounter = data.encounters[i];
+
+ latestSeenAt =
+ latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
+
+ if (!(encounter.company!.name in companyCounts)) {
+ companyCounts[encounter.company!.name] = 1;
+ }
+ companyCounts[encounter.company!.name] += 1;
+
+ if (!(encounter.location in locationCounts)) {
+ locationCounts[encounter.location] = 1;
+ }
+ locationCounts[encounter.location] += 1;
+
+ if (!(encounter.role in roleCounts)) {
+ roleCounts[encounter.role] = 1;
+ }
+ roleCounts[encounter.role] += 1;
+ }
+
const question: Question = {
- company: data.encounters[0].company!.name ?? 'Unknown company',
+ aggregatedQuestionEncounters: {
+ companyCounts,
+ latestSeenAt,
+ locationCounts,
+ roleCounts,
+ },
content: data.content,
id: data.id,
- location: data.encounters[0].location ?? 'Unknown location',
numAnswers: data._count.answers,
numComments: data._count.comments,
numVotes: votes,
receivedCount: data.encounters.length,
- role: data.encounters[0].role ?? 'Unknown role',
- seenAt: data.encounters[0].seenAt,
+ seenAt: latestSeenAt,
type: data.questionType,
updatedAt: data.updatedAt,
user: data.user?.name ?? '',
};
return question;
});
+
+ let nextCursor: typeof cursor | undefined = undefined;
+
+ if (questionsData.length > input.limit) {
+ const nextItem = questionsData.pop()!;
+ processedQuestionsData.pop();
+
+ const nextIdCursor: string | undefined = nextItem.id;
+ const nextLastSeenCursor =
+ input.sortType === SortType.NEW ? nextItem.lastSeenAt : undefined;
+ const nextUpvoteCursor =
+ input.sortType === SortType.TOP ? nextItem.upvotes : undefined;
+
+ nextCursor = {
+ idCursor: nextIdCursor,
+ lastSeenCursor: nextLastSeenCursor,
+ upvoteCursor: nextUpvoteCursor,
+ };
+ }
+
+ return {
+ data: processedQuestionsData,
+ nextCursor,
+ };
},
})
.query('getQuestionById', {
@@ -190,16 +269,45 @@ export const questionsQuestionRouter = createProtectedRouter()
0,
);
+ const companyCounts: Record = {};
+ const locationCounts: Record = {};
+ const roleCounts: Record = {};
+
+ let latestSeenAt = questionData.encounters[0].seenAt;
+
+ for (const encounter of questionData.encounters) {
+ latestSeenAt =
+ latestSeenAt < encounter.seenAt ? encounter.seenAt : latestSeenAt;
+
+ if (!(encounter.company!.name in companyCounts)) {
+ companyCounts[encounter.company!.name] = 1;
+ }
+ companyCounts[encounter.company!.name] += 1;
+
+ if (!(encounter.location in locationCounts)) {
+ locationCounts[encounter.location] = 1;
+ }
+ locationCounts[encounter.location] += 1;
+
+ if (!(encounter.role in roleCounts)) {
+ roleCounts[encounter.role] = 1;
+ }
+ roleCounts[encounter.role] += 1;
+ }
+
const question: Question = {
- company: questionData.encounters[0].company!.name ?? 'Unknown company',
+ aggregatedQuestionEncounters: {
+ companyCounts,
+ latestSeenAt,
+ locationCounts,
+ roleCounts,
+ },
content: questionData.content,
id: questionData.id,
- location: questionData.encounters[0].location ?? 'Unknown location',
numAnswers: questionData._count.answers,
numComments: questionData._count.comments,
numVotes: votes,
receivedCount: questionData.encounters.length,
- role: questionData.encounters[0].role ?? 'Unknown role',
seenAt: questionData.encounters[0].seenAt,
type: questionData.questionType,
updatedAt: questionData.updatedAt,
diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts
index 487ae0c9..f2b26332 100644
--- a/apps/portal/src/types/offers.d.ts
+++ b/apps/portal/src/types/offers.d.ts
@@ -26,7 +26,6 @@ export type Experience = {
level: string?;
location: string?;
monthlySalary: Valuation?;
- specialization: string?;
title: string?;
totalCompensation: Valuation?;
};
@@ -87,12 +86,11 @@ export type ProfileOffer = {
};
export type FullTime = {
- baseSalary: Valuation;
- bonus: Valuation;
+ baseSalary: Valuation?;
+ bonus: Valuation?;
id: string;
level: string;
- specialization: string;
- stocks: Valuation;
+ stocks: Valuation?;
title: string;
totalCompensation: Valuation;
};
@@ -101,7 +99,6 @@ export type Intern = {
id: string;
internshipCycle: string;
monthlySalary: Valuation;
- specialization: string;
startYear: number;
title: string;
};
@@ -163,7 +160,6 @@ export type AnalysisHighestOffer = {
id: string;
level: string;
location: string;
- specialization: string;
totalYoe: number;
};
@@ -178,7 +174,6 @@ export type AnalysisOffer = {
negotiationStrategy: string;
previousCompanies: Array;
profileName: string;
- specialization: string;
title: string;
totalYoe: number;
};
diff --git a/apps/portal/src/types/questions.d.ts b/apps/portal/src/types/questions.d.ts
index 4157eb37..aea8d31e 100644
--- a/apps/portal/src/types/questions.d.ts
+++ b/apps/portal/src/types/questions.d.ts
@@ -1,16 +1,13 @@
import type { QuestionsQuestionType } from '@prisma/client';
export type Question = {
- // TODO: company, location, role maps
- company: string;
+ aggregatedQuestionEncounters: AggregatedQuestionEncounter;
content: string;
id: string;
- location: string;
numAnswers: number;
numComments: number;
numVotes: number;
receivedCount: number;
- role: string;
seenAt: Date;
type: QuestionsQuestionType;
updatedAt: Date;
@@ -19,6 +16,7 @@ export type Question = {
export type AggregatedQuestionEncounter = {
companyCounts: Record;
+ latestSeenAt: Date;
locationCounts: Record;
roleCounts: Record;
};
diff --git a/apps/portal/src/utils/offers/time.tsx b/apps/portal/src/utils/offers/time.tsx
index 4f68adeb..ffa3c545 100644
--- a/apps/portal/src/utils/offers/time.tsx
+++ b/apps/portal/src/utils/offers/time.tsx
@@ -25,9 +25,11 @@ export function timeSinceNow(date: Date | number | string) {
}
interval = seconds / 60;
if (interval > 1) {
- return `${Math.floor(interval)} minutes`;
+ const time: number = Math.floor(interval);
+ return time === 1 ? `${time} minute` : `${time} minutes`;
}
- return `${Math.floor(interval)} seconds`;
+ const time: number = Math.floor(interval);
+ return time === 1 ? `${time} second` : `${time} seconds`;
}
export function formatDate(value: Date | number | string) {
diff --git a/packages/ui/src/CheckboxInput/CheckboxInput.tsx b/packages/ui/src/CheckboxInput/CheckboxInput.tsx
index edeb9d3a..fec43b7e 100644
--- a/packages/ui/src/CheckboxInput/CheckboxInput.tsx
+++ b/packages/ui/src/CheckboxInput/CheckboxInput.tsx
@@ -7,6 +7,7 @@ type Props = Readonly<{
defaultValue?: boolean;
description?: string;
disabled?: boolean;
+ errorMessage?: string;
label: string;
name?: string;
onChange?: (
@@ -21,6 +22,7 @@ function CheckboxInput(
defaultValue,
description,
disabled = false,
+ errorMessage,
label,
name,
value,
@@ -30,59 +32,67 @@ function CheckboxInput(
) {
const id = useId();
const descriptionId = useId();
+ const errorId = useId();
return (
-
-
- {
- onChange?.(event.target.checked, event);
- }
- : undefined
- }
- />
-
-
-
- {label}
-
- {description && (
-
+
+
+
- {description}
-
- )}
+ defaultChecked={defaultValue}
+ disabled={disabled}
+ id={id}
+ name={name}
+ type="checkbox"
+ onChange={(event) => {
+ if (!onChange) {
+ return;
+ }
+
+ onChange(event.target.checked, event);
+ }}
+ />
+
+
+
+ {label}
+
+ {description && (
+
+ {description}
+
+ )}
+
+ {errorMessage && (
+
+ {errorMessage}
+
+ )}
);
}
diff --git a/packages/ui/src/TextInput/TextInput.tsx b/packages/ui/src/TextInput/TextInput.tsx
index 98b150f2..81f64190 100644
--- a/packages/ui/src/TextInput/TextInput.tsx
+++ b/packages/ui/src/TextInput/TextInput.tsx
@@ -154,14 +154,14 @@ function TextInput(
switch (startAddOnType) {
case 'label':
return (
-
+
{startAddOn}
);
case 'icon': {
const StartAddOn = startAddOn;
return (
-