-
+
{company.name}
-
+
{getLabelForJobTitleType(title as JobTitleType)}
- {totalYoe}
- {convertMoneyToString(income)}
- {formatDate(monthYearReceived)}
+ {totalYoe}
+ {convertMoneyToString(income)}
+ {jobType === JobType.FULLTIME && (
+
+ {`${baseSalary && convertMoneyToString(baseSalary)} / ${
+ bonus && convertMoneyToString(bonus)
+ } / ${stocks && convertMoneyToString(stocks)}`}
+
+ )}
+ {formatDate(monthYearReceived)}
;
export default function OffersTable({
- cityFilter,
+ countryFilter,
companyFilter,
jobTitleFilter,
}: OffersTableProps) {
const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location
- const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY);
+ const [selectedYoe, setSelectedYoe] = useState('');
+ const [jobType, setJobType] = useState(JobType.FULLTIME);
const [pagination, setPagination] = useState({
currentPage: 0,
numOfItems: 0,
@@ -43,6 +47,10 @@ export default function OffersTable({
OfferTableFilterOptions[0].value,
);
const { event: gaEvent } = useGoogleAnalytics();
+ const router = useRouter();
+ const { yoeCategory = '' } = router.query;
+ const [isLoading, setIsLoading] = useState(true);
+
useEffect(() => {
setPagination({
currentPage: 0,
@@ -50,20 +58,26 @@ export default function OffersTable({
numOfPages: 0,
totalItems: 0,
});
- }, [selectedTab, currency]);
- const offersQuery = trpc.useQuery(
+ setIsLoading(true);
+ }, [selectedYoe, currency, countryFilter, companyFilter, jobTitleFilter]);
+
+ useEffect(() => {
+ setSelectedYoe(yoeCategory as YOE_CATEGORY);
+ event?.preventDefault();
+ }, [yoeCategory]);
+
+ trpc.useQuery(
[
'offers.list',
{
- // Location: 'Singapore, Singapore', // TODO: Geolocation
- cityId: cityFilter,
companyId: companyFilter,
+ countryId: countryFilter,
currency,
limit: NUMBER_OF_OFFERS_IN_PAGE,
offset: pagination.currentPage,
sortBy: OfferTableSortBy[selectedFilter] ?? '-monthYearReceived',
title: jobTitleFilter,
- yoeCategory: selectedTab,
+ yoeCategory: YOE_CATEGORY_PARAM[yoeCategory as string] ?? undefined,
},
],
{
@@ -73,28 +87,52 @@ export default function OffersTable({
onSuccess: (response: GetOffersResponse) => {
setOffers(response.data);
setPagination(response.paging);
+ setJobType(response.jobType);
+ setIsLoading(false);
},
},
);
function renderFilters() {
return (
-
+
itemValue === selectedTab,
+ OfferTableYoeOptions.filter(
+ ({ value: itemValue }) => itemValue === selectedYoe,
)[0].label
}
size="inherit">
- {OfferTableTabOptions.map(({ label: itemLabel, value }) => (
+ {OfferTableYoeOptions.map(({ label: itemLabel, value }) => (
{
- setSelectedTab(value);
+ if (value === '') {
+ router.replace(
+ {
+ pathname: router.pathname,
+ query: undefined,
+ },
+ undefined,
+ // Do not refresh the page
+ { shallow: true },
+ );
+ } else {
+ const params = new URLSearchParams({
+ ['yoeCategory']: value,
+ });
+ router.replace(
+ {
+ pathname: location.pathname,
+ search: params.toString(),
+ },
+ undefined,
+ { shallow: true },
+ );
+ }
gaEvent({
action: `offers.table_filter_yoe_category_${value}`,
category: 'engagement',
@@ -104,9 +142,11 @@ export default function OffersTable({
/>
))}
-
+
-
Display offers in
+
+ Display offers in
+
setCurrency(value)}
selectedCurrency={currency}
@@ -139,14 +179,26 @@ export default function OffersTable({
}
function renderHeader() {
- const columns = [
+ let columns = [
'Company',
'Title',
'YOE',
- selectedTab === YOE_CATEGORY.INTERN ? 'Monthly Salary' : 'Annual TC',
+ selectedYoe === YOE_CATEGORY.INTERN ? 'Monthly Salary' : 'Annual TC',
'Date Offered',
'Actions',
];
+ if (jobType === JobType.FULLTIME) {
+ columns = [
+ 'Company',
+ 'Title',
+ 'YOE',
+ 'Annual TC',
+ 'Annual Base / Bonus / Stocks',
+ 'Date Offered',
+ 'Actions',
+ ];
+ }
+
return (
@@ -154,7 +206,7 @@ export default function OffersTable({
-
- {renderFilters()}
- {offersQuery.isLoading ? (
-
-
-
- ) : (
-
-
- {renderHeader()}
-
- {offers.map((offer) => (
-
- ))}
-
-
-
- )}
-
-
+
+ {renderFilters()}
+ {isLoading ? (
+
+
+
+ ) : (
+
+
+ {renderHeader()}
+
+ {offers.map((offer) => (
+
+ ))}
+
+
+ {!offers ||
+ (offers.length === 0 && (
+
+
No data yet🥺
+
+ Please try another set of filters.
+
+
+ ))}
+
+ )}
+
);
}
diff --git a/apps/portal/src/components/offers/table/OffersTablePagination.tsx b/apps/portal/src/components/offers/table/OffersTablePagination.tsx
index 1a24a045..9a235901 100644
--- a/apps/portal/src/components/offers/table/OffersTablePagination.tsx
+++ b/apps/portal/src/components/offers/table/OffersTablePagination.tsx
@@ -1,3 +1,4 @@
+import { useEffect, useState } from 'react';
import { Pagination } from '@tih/ui';
import type { Paging } from '~/types/offers';
@@ -15,30 +16,36 @@ export default function OffersTablePagination({
startNumber,
handlePageChange,
}: OffersTablePaginationProps) {
+ const [screenWidth, setScreenWidth] = useState(0);
+ useEffect(() => {
+ setScreenWidth(window.innerWidth);
+ }, []);
return (
-
-
- Showing
-
- {` ${startNumber} - ${endNumber} `}
-
- {`of `}
-
- {pagination.totalItems}
-
-
- {
- handlePageChange(currPage - 1);
- }}
- />
+
+
+
+ Showing
+
+ {` ${startNumber} - ${endNumber} `}
+
+ {`of `}
+
+ {pagination.totalItems}
+
+
+
+
500 ? 2 : 0}
+ start={1}
+ onSelect={(currPage) => {
+ handlePageChange(currPage - 1);
+ }}
+ />
+
+
);
}
diff --git a/apps/portal/src/components/offers/table/types.ts b/apps/portal/src/components/offers/table/types.ts
index ca14aa32..7fb5ad17 100644
--- a/apps/portal/src/components/offers/table/types.ts
+++ b/apps/portal/src/components/offers/table/types.ts
@@ -1,12 +1,20 @@
// eslint-disable-next-line no-shadow
export enum YOE_CATEGORY {
- INTERN = 0,
- ENTRY = 1,
- MID = 2,
- SENIOR = 3,
+ ENTRY = 'entry',
+ INTERN = 'intern',
+ MID = 'mid',
+ SENIOR = 'senior',
}
-export const OfferTableTabOptions = [
+export const YOE_CATEGORY_PARAM: Record = {
+ entry: 1,
+ intern: 0,
+ mid: 2,
+ senior: 3,
+};
+
+export const OfferTableYoeOptions = [
+ { label: 'All Full Time YOE', value: '' },
{
label: 'Fresh Grad (0-2 YOE)',
value: YOE_CATEGORY.ENTRY,
diff --git a/apps/portal/src/components/questions/ContributeQuestionCard.tsx b/apps/portal/src/components/questions/ContributeQuestionCard.tsx
index ae53a914..f91e1eb4 100644
--- a/apps/portal/src/components/questions/ContributeQuestionCard.tsx
+++ b/apps/portal/src/components/questions/ContributeQuestionCard.tsx
@@ -30,54 +30,56 @@ export default function ContributeQuestionCard({
});
return (
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Contribute
+
-
- Contribute
-
-
+
-
+
);
}
diff --git a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx
index ae08724b..8e481632 100644
--- a/apps/portal/src/components/questions/ContributeQuestionDialog.tsx
+++ b/apps/portal/src/components/questions/ContributeQuestionDialog.tsx
@@ -35,7 +35,12 @@ export default function ContributeQuestionDialog({
return (
-
+ {
+ onCancel();
+ }}>
(
defaultCompany,
);
- const [location, setLocation] = useState(
- defaultLocation,
- );
+ const [location, setLocation] = useState<
+ (Location & TypeaheadOption) | undefined
+ >(defaultLocation);
const [questionType, setQuestionType] =
useState('CODING');
@@ -41,7 +46,7 @@ export default function LandingComponent({
setCompany(newCompany);
};
- const handleChangeLocation = (newLocation: FilterChoice) => {
+ const handleChangeLocation = (newLocation: Location & TypeaheadOption) => {
setLocation(newLocation);
};
@@ -71,7 +76,7 @@ export default function LandingComponent({
className="h-40 w-40"
src="/bank-logo.png"
/>
-
+
Tech Interview Question Bank
@@ -101,7 +106,6 @@ export default function LandingComponent({
isLabelHidden={true}
value={company}
onSelect={(value) => {
- // @ts-ignore TODO(questions): handle potentially null value.
handleChangeCompany(value);
}}
/>
@@ -110,7 +114,6 @@ export default function LandingComponent({
isLabelHidden={true}
value={location}
onSelect={(value) => {
- // @ts-ignore TODO(questions): handle potentially null value.
handleChangeLocation(value);
}}
/>
@@ -124,8 +127,8 @@ export default function LandingComponent({
onClick={() => {
if (company !== undefined && location !== undefined) {
return handleLandingQuery({
- company: company.label,
- location: location.value,
+ companySlug: companyOptionToSlug(company),
+ location: locationOptionToSlug(location),
questionType,
});
}
diff --git a/apps/portal/src/components/questions/QuestionSearchBar.tsx b/apps/portal/src/components/questions/QuestionSearchBar.tsx
index 917c342d..bafcb3cb 100644
--- a/apps/portal/src/components/questions/QuestionSearchBar.tsx
+++ b/apps/portal/src/components/questions/QuestionSearchBar.tsx
@@ -9,10 +9,14 @@ import SortOptionsSelect from './SortOptionsSelect';
export type QuestionSearchBarProps = SortOptionsSelectProps & {
onFilterOptionsToggle: () => void;
+ onQueryChange: (query: string) => void;
+ query: string;
};
export default function QuestionSearchBar({
onFilterOptionsToggle,
+ onQueryChange,
+ query,
...sortOptionsSelectProps
}: QuestionSearchBarProps) {
return (
@@ -24,6 +28,10 @@ export default function QuestionSearchBar({
placeholder="Search by content"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
+ value={query}
+ onChange={(value) => {
+ onQueryChange(value);
+ }}
/>
diff --git a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx
index 0c8f1460..7b50c1a9 100644
--- a/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx
+++ b/apps/portal/src/components/questions/card/question/BaseQuestionCard.tsx
@@ -216,7 +216,11 @@ export default function BaseQuestionCard({
/>
)}
-
+
{content}
{!showReceivedForm &&
diff --git a/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx b/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx
index faf8d25d..10aa33ef 100644
--- a/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx
+++ b/apps/portal/src/components/questions/forms/ContributeQuestionForm.tsx
@@ -128,7 +128,6 @@ export default function ContributeQuestionForm({
{...field}
required={true}
onSelect={(option) => {
- // @ts-ignore TODO(questions): handle potentially null value.
field.onChange(option);
}}
/>
@@ -141,6 +140,7 @@ export default function ContributeQuestionForm({
name="date"
render={({ field }) => (
{
field.onChange(id);
}}
@@ -181,7 +180,6 @@ export default function ContributeQuestionForm({
{...field}
required={true}
onSelect={(option) => {
- // @ts-ignore TODO(questions): handle potentially null value.
field.onChange(option);
}}
/>
@@ -278,6 +276,7 @@ export default function ContributeQuestionForm({
diff --git a/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx
index 7a4f283e..453733c8 100644
--- a/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx
+++ b/apps/portal/src/components/questions/forms/CreateQuestionEncounterForm.tsx
@@ -42,14 +42,16 @@ export default function CreateQuestionEncounterForm({
return (
-
I saw this question at
+
+ I saw this question {step <= 1 ? 'at' : step === 2 ? 'for' : 'on'}
+
{step === 0 && (
{
setSelectedCompany(company);
}}
@@ -64,9 +66,8 @@ export default function CreateQuestionEncounterForm({
{
setSelectedLocation(location);
}}
@@ -81,9 +82,8 @@ export default function CreateQuestionEncounterForm({
{
setSelectedRole(role);
}}
@@ -96,6 +96,7 @@ export default function CreateQuestionEncounterForm({
)}
{step === 3 && (
;
type TypeaheadOption = TypeaheadProps['options'][number];
-export type ExpandedTypeaheadProps = Omit &
+export type ExpandedTypeaheadProps = Omit<
+ TypeaheadProps,
+ 'nullable' | 'onSelect'
+> &
RequireAllOrNone<{
clearOnSelect?: boolean;
filterOption: (option: TypeaheadOption) => boolean;
@@ -59,8 +62,7 @@ export default function ExpandedTypeahead({
if (clearOnSelect) {
setKey((key + 1) % 2);
}
- // TODO: Remove onSelect null coercion once onSelect prop is refactored
- onSelect(option!);
+ onSelect(option);
}}
/>
diff --git a/apps/portal/src/components/resumes/ResumesNavigation.ts b/apps/portal/src/components/resumes/ResumesNavigation.tsx
similarity index 84%
rename from apps/portal/src/components/resumes/ResumesNavigation.ts
rename to apps/portal/src/components/resumes/ResumesNavigation.tsx
index f26b0c6e..ba39547c 100644
--- a/apps/portal/src/components/resumes/ResumesNavigation.ts
+++ b/apps/portal/src/components/resumes/ResumesNavigation.tsx
@@ -22,6 +22,13 @@ const navigation: ProductNavigationItems = [
const config = {
googleAnalyticsMeasurementID: 'G-VFTWPMW1WK',
+ logo: (
+
+ ),
navigation,
showGlobalNav: false,
title: 'Resumes',
diff --git a/apps/portal/src/components/resumes/comments/comment/ResumeCommentVoteButtons.tsx b/apps/portal/src/components/resumes/comments/comment/ResumeCommentVoteButtons.tsx
index 9c2435e6..c7956fbe 100644
--- a/apps/portal/src/components/resumes/comments/comment/ResumeCommentVoteButtons.tsx
+++ b/apps/portal/src/components/resumes/comments/comment/ResumeCommentVoteButtons.tsx
@@ -8,6 +8,7 @@ import {
import { Vote } from '@prisma/client';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
+import loginPageHref from '~/components/shared/loginPageHref';
import { trpc } from '~/utils/trpc';
@@ -63,7 +64,7 @@ export default function ResumeCommentVoteButtons({
const onVote = async (value: Vote, setAnimation: (_: boolean) => void) => {
if (!userId) {
- router.push('/api/auth/signin');
+ router.push(loginPageHref());
return;
}
diff --git a/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx b/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx
index c470dffb..ce390fb7 100644
--- a/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx
+++ b/apps/portal/src/components/resumes/shared/ResumeSignInButton.tsx
@@ -1,5 +1,7 @@
import clsx from 'clsx';
-import { signIn } from 'next-auth/react';
+import Link from 'next/link';
+
+import loginPageHref from '~/components/shared/loginPageHref';
type Props = Readonly<{
className?: string;
@@ -10,15 +12,11 @@ export default function ResumeSignInButton({ text, className }: Props) {
return (
diff --git a/apps/portal/src/components/shared/CitiesTypeahead.tsx b/apps/portal/src/components/shared/CitiesTypeahead.tsx
index 05e527ca..902759b0 100644
--- a/apps/portal/src/components/shared/CitiesTypeahead.tsx
+++ b/apps/portal/src/components/shared/CitiesTypeahead.tsx
@@ -1,28 +1,32 @@
+import type { ComponentProps } from 'react';
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { trpc } from '~/utils/trpc';
-type Props = Readonly<{
- disabled?: boolean;
- errorMessage?: string;
- isLabelHidden?: boolean;
- label?: string;
- onSelect: (option: TypeaheadOption | null) => void;
- placeholder?: string;
- required?: boolean;
- value?: TypeaheadOption | null;
-}>;
+type BaseProps = Pick<
+ ComponentProps,
+ | 'disabled'
+ | 'errorMessage'
+ | 'isLabelHidden'
+ | 'placeholder'
+ | 'required'
+ | 'textSize'
+>;
+
+type Props = BaseProps &
+ Readonly<{
+ label?: string;
+ onSelect: (option: TypeaheadOption | null) => void;
+ value?: TypeaheadOption | null;
+ }>;
export default function CitiesTypeahead({
- disabled,
label = 'City',
onSelect,
- isLabelHidden,
- placeholder,
- required,
value,
+ ...props
}: Props) {
const [query, setQuery] = useState('');
const cities = trpc.useQuery([
@@ -36,8 +40,6 @@ export default function CitiesTypeahead({
return (
);
}
diff --git a/apps/portal/src/components/shared/CompaniesTypeahead.tsx b/apps/portal/src/components/shared/CompaniesTypeahead.tsx
index 619d9a55..76d35557 100644
--- a/apps/portal/src/components/shared/CompaniesTypeahead.tsx
+++ b/apps/portal/src/components/shared/CompaniesTypeahead.tsx
@@ -1,26 +1,30 @@
+import type { ComponentProps } from 'react';
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { trpc } from '~/utils/trpc';
-type Props = Readonly<{
- disabled?: boolean;
- errorMessage?: string;
- isLabelHidden?: boolean;
- onSelect: (option: TypeaheadOption | null) => void;
- placeholder?: string;
- required?: boolean;
- value?: TypeaheadOption | null;
-}>;
+type BaseProps = Pick<
+ ComponentProps,
+ | 'disabled'
+ | 'errorMessage'
+ | 'isLabelHidden'
+ | 'placeholder'
+ | 'required'
+ | 'textSize'
+>;
+
+type Props = BaseProps &
+ Readonly<{
+ onSelect: (option: TypeaheadOption | null) => void;
+ value?: TypeaheadOption | null;
+ }>;
export default function CompaniesTypeahead({
- disabled,
onSelect,
- isLabelHidden,
- placeholder,
- required,
value,
+ ...props
}: Props) {
const [query, setQuery] = useState('');
const companies = trpc.useQuery([
@@ -34,8 +38,6 @@ export default function CompaniesTypeahead({
return (
);
}
diff --git a/apps/portal/src/components/shared/Container.tsx b/apps/portal/src/components/shared/Container.tsx
new file mode 100644
index 00000000..3dff4111
--- /dev/null
+++ b/apps/portal/src/components/shared/Container.tsx
@@ -0,0 +1,27 @@
+import clsx from 'clsx';
+import React from 'react';
+
+type Props = Readonly<{
+ children: React.ReactNode;
+ className?: string;
+ variant?: 'md' | 'sm' | 'xs';
+}>;
+
+export default function Container({
+ children,
+ className,
+ variant = 'md',
+}: Props) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/portal/src/components/shared/CountriesTypeahead.tsx b/apps/portal/src/components/shared/CountriesTypeahead.tsx
index c3bae569..51d72cb0 100644
--- a/apps/portal/src/components/shared/CountriesTypeahead.tsx
+++ b/apps/portal/src/components/shared/CountriesTypeahead.tsx
@@ -1,26 +1,30 @@
+import type { ComponentProps } from 'react';
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { trpc } from '~/utils/trpc';
-type Props = Readonly<{
- disabled?: boolean;
- errorMessage?: string;
- isLabelHidden?: boolean;
- onSelect: (option: TypeaheadOption | null) => void;
- placeholder?: string;
- required?: boolean;
- value?: TypeaheadOption | null;
-}>;
+type BaseProps = Pick<
+ ComponentProps,
+ | 'disabled'
+ | 'errorMessage'
+ | 'isLabelHidden'
+ | 'placeholder'
+ | 'required'
+ | 'textSize'
+>;
+
+type Props = BaseProps &
+ Readonly<{
+ onSelect: (option: TypeaheadOption | null) => void;
+ value?: TypeaheadOption | null;
+ }>;
export default function CountriesTypeahead({
- disabled,
onSelect,
- isLabelHidden,
- placeholder,
- required,
value,
+ ...props
}: Props) {
const [query, setQuery] = useState('');
const countries = trpc.useQuery([
@@ -34,8 +38,6 @@ export default function CountriesTypeahead({
return (
);
}
diff --git a/apps/portal/src/components/shared/JobTitlesTypahead.tsx b/apps/portal/src/components/shared/JobTitlesTypahead.tsx
index b5221262..9de68c45 100644
--- a/apps/portal/src/components/shared/JobTitlesTypahead.tsx
+++ b/apps/portal/src/components/shared/JobTitlesTypahead.tsx
@@ -1,25 +1,30 @@
+import type { ComponentProps } from 'react';
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Typeahead } from '@tih/ui';
import { JobTitleLabels } from './JobTitles';
-type Props = Readonly<{
- disabled?: boolean;
- isLabelHidden?: boolean;
- onSelect: (option: TypeaheadOption | null) => void;
- placeholder?: string;
- required?: boolean;
- value?: TypeaheadOption | null;
-}>;
+type BaseProps = Pick<
+ ComponentProps,
+ | 'disabled'
+ | 'errorMessage'
+ | 'isLabelHidden'
+ | 'placeholder'
+ | 'required'
+ | 'textSize'
+>;
+
+type Props = BaseProps &
+ Readonly<{
+ onSelect: (option: TypeaheadOption | null) => void;
+ value?: TypeaheadOption | null;
+ }>;
export default function JobTitlesTypeahead({
- disabled,
onSelect,
- isLabelHidden,
- placeholder,
- required,
value,
+ ...props
}: Props) {
const [query, setQuery] = useState('');
const options = Object.entries(JobTitleLabels)
@@ -35,18 +40,14 @@ export default function JobTitlesTypeahead({
return (
);
}
diff --git a/apps/portal/src/components/shared/MonthYearPicker.tsx b/apps/portal/src/components/shared/MonthYearPicker.tsx
index aa3fb1d8..0612455c 100644
--- a/apps/portal/src/components/shared/MonthYearPicker.tsx
+++ b/apps/portal/src/components/shared/MonthYearPicker.tsx
@@ -1,3 +1,4 @@
+import clsx from 'clsx';
import { useEffect, useId, useState } from 'react';
import { Select } from '@tih/ui';
@@ -14,6 +15,7 @@ export type MonthYearOptional = Readonly<{
}>;
type Props = Readonly<{
+ className?: string;
errorMessage?: string;
monthLabel?: string;
monthRequired?: boolean;
@@ -84,6 +86,7 @@ const YEAR_OPTIONS = Array.from({ length: NUM_YEARS }, (_, i) => {
});
export default function MonthYearPicker({
+ className,
errorMessage,
monthLabel = 'Month',
value,
@@ -109,29 +112,35 @@ export default function MonthYearPicker({
return (
-
-
- onChange({ month: Number(newMonth) as Month, year: value.year })
- }
- />
-
- onChange({ month: value.month, year: Number(newYear) })
- }
- />
+
+
+
+ onChange({ month: Number(newMonth) as Month, year: value.year })
+ }
+ />
+
+
+
+ onChange({ month: value.month, year: Number(newYear) })
+ }
+ />
+
{errorMessage && (
diff --git a/apps/portal/src/components/shared/icons/GitHubIcon.tsx b/apps/portal/src/components/shared/icons/GitHubIcon.tsx
new file mode 100644
index 00000000..540cd6a3
--- /dev/null
+++ b/apps/portal/src/components/shared/icons/GitHubIcon.tsx
@@ -0,0 +1,15 @@
+export default function GitHubIcon(props: React.ComponentProps<'svg'>) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/portal/src/components/shared/loginPageHref.ts b/apps/portal/src/components/shared/loginPageHref.ts
new file mode 100644
index 00000000..bf505b4e
--- /dev/null
+++ b/apps/portal/src/components/shared/loginPageHref.ts
@@ -0,0 +1,11 @@
+export default function loginPageHref(redirectUrl?: string) {
+ return {
+ pathname: '/login',
+ query: {
+ redirect:
+ typeof window !== 'undefined'
+ ? redirectUrl ?? window.location.href
+ : null,
+ },
+ };
+}
diff --git a/apps/portal/src/mappers/offers-mappers.ts b/apps/portal/src/mappers/offers-mappers.ts
index e0cea797..504e9400 100644
--- a/apps/portal/src/mappers/offers-mappers.ts
+++ b/apps/portal/src/mappers/offers-mappers.ts
@@ -700,6 +700,25 @@ export const dashboardOfferDtoMapper = (
dashboardOfferDto.income = valuationDtoMapper(
offer.offersFullTime.totalCompensation,
);
+
+ if (offer.offersFullTime.baseSalary) {
+ dashboardOfferDto.baseSalary = valuationDtoMapper(
+ offer.offersFullTime.baseSalary
+ );
+ }
+
+ if (offer.offersFullTime.bonus) {
+ dashboardOfferDto.bonus = valuationDtoMapper(
+ offer.offersFullTime.bonus
+ );
+
+ }
+
+ if (offer.offersFullTime.stocks) {
+ dashboardOfferDto.stocks = valuationDtoMapper(
+ offer.offersFullTime.stocks
+ );
+ }
} else if (offer.offersIntern) {
dashboardOfferDto.income = valuationDtoMapper(
offer.offersIntern.monthlySalary,
@@ -712,10 +731,12 @@ export const dashboardOfferDtoMapper = (
export const getOffersResponseMapper = (
data: Array,
paging: Paging,
+ jobType: JobType
) => {
const getOffersResponse: GetOffersResponse = {
data,
- paging,
+ jobType,
+ paging
};
return getOffersResponse;
};
diff --git a/apps/portal/src/pages/_document.tsx b/apps/portal/src/pages/_document.tsx
index 69112ab2..016ecd1a 100644
--- a/apps/portal/src/pages/_document.tsx
+++ b/apps/portal/src/pages/_document.tsx
@@ -2,9 +2,9 @@ import { Head, Html, Main, NextScript } from 'next/document';
export default function Document() {
return (
-
+
-
+
diff --git a/apps/portal/src/pages/login.tsx b/apps/portal/src/pages/login.tsx
new file mode 100644
index 00000000..8223c02c
--- /dev/null
+++ b/apps/portal/src/pages/login.tsx
@@ -0,0 +1,72 @@
+import { useRouter } from 'next/router';
+import type {
+ GetServerSideProps,
+ InferGetServerSidePropsType,
+} from 'next/types';
+import { getProviders, signIn } from 'next-auth/react';
+import { Button } from '@tih/ui';
+
+import GitHubIcon from '~/components/shared/icons/GitHubIcon';
+
+export const getServerSideProps: GetServerSideProps<{
+ providers: Awaited>;
+}> = async () => {
+ const providers = await getProviders();
+ return {
+ props: { providers },
+ };
+};
+
+export default function LoginPage({
+ providers,
+}: InferGetServerSidePropsType) {
+ const router = useRouter();
+
+ return (
+
+
+
+
+
+ Tech Interview Handbook Portal
+
+
+ Get your resumes peer-reviewed, discuss solutions to tech interview
+ questions, get offer data points.
+
+
+
+
+ {providers != null &&
+ Object.values(providers).map((provider) => (
+
+
+ signIn(
+ provider.id,
+ router.query.redirect != null
+ ? {
+ callbackUrl: String(router.query.redirect),
+ }
+ : undefined,
+ )
+ }
+ />
+
+ ))}
+
+
+
+
+ );
+}
diff --git a/apps/portal/src/pages/offers/dashboard.tsx b/apps/portal/src/pages/offers/dashboard.tsx
index 494748c7..1ae9ea46 100644
--- a/apps/portal/src/pages/offers/dashboard.tsx
+++ b/apps/portal/src/pages/offers/dashboard.tsx
@@ -3,7 +3,8 @@ import { signIn, useSession } from 'next-auth/react';
import { useState } from 'react';
import { Button, Spinner } from '@tih/ui';
-import DashboardOfferCard from '~/components/offers/dashboard/DashboardProfileCard';
+import DashboardProfileCard from '~/components/offers/dashboard/DashboardProfileCard';
+import Container from '~/components/shared/Container';
import { trpc } from '~/utils/trpc';
@@ -30,19 +31,21 @@ export default function ProfilesDashboard() {
if (status === 'loading' || userProfilesQuery.isLoading) {
return (
-
+
);
}
+
if (status === 'unauthenticated') {
signIn();
}
+
if (userProfiles.length === 0) {
return (
-
+
You have not saved any offer profiles yet.
@@ -59,37 +62,36 @@ export default function ProfilesDashboard() {
);
}
+
return (
- <>
+
{userProfilesQuery.isLoading && (
-
+
)}
{!userProfilesQuery.isLoading && (
-
-
+
+
Your dashboard
-
- Save your offer profiles to dashboard to easily access and edit them
- later.
+
+ Save your offer profiles to your dashboard to easily access and edit
+ them later.
-
-
+
+
{userProfiles?.map((profile) => (
-
-
+
+
))}
)}
- >
+
);
}
diff --git a/apps/portal/src/pages/offers/index.tsx b/apps/portal/src/pages/offers/index.tsx
index ad0ff41c..9eaeb955 100644
--- a/apps/portal/src/pages/offers/index.tsx
+++ b/apps/portal/src/pages/offers/index.tsx
@@ -5,14 +5,16 @@ import { Banner } from '@tih/ui';
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import OffersTable from '~/components/offers/table/OffersTable';
-import CitiesTypeahead from '~/components/shared/CitiesTypeahead';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
+import Container from '~/components/shared/Container';
+import CountriesTypeahead from '~/components/shared/CountriesTypeahead';
+import type { JobTitleType } from '~/components/shared/JobTitles';
import JobTitlesTypeahead from '~/components/shared/JobTitlesTypahead';
export default function OffersHomePage() {
- const [jobTitleFilter, setjobTitleFilter] = useState('software-engineer');
+ const [jobTitleFilter, setJobTitleFilter] = useState
('');
const [companyFilter, setCompanyFilter] = useState('');
- const [cityFilter, setCityFilter] = useState('');
+ const [countryFilter, setCountryFilter] = useState('');
const { event: gaEvent } = useGoogleAnalytics();
return (
@@ -24,21 +26,23 @@ export default function OffersHomePage() {
. ⭐
-
+
-
{
if (option) {
- setCityFilter(option.value);
+ setCountryFilter(option.value);
gaEvent({
- action: `offers.table_filter_city_${option.value}`,
+ action: `offers.table_filter_country_${option.value}`,
category: 'engagement',
- label: 'Filter by city',
+ label: 'Filter by country',
});
+ } else {
+ setCountryFilter('');
}
}}
/>
@@ -60,15 +64,18 @@ export default function OffersHomePage() {
{
if (option) {
- setjobTitleFilter(option.value);
+ setJobTitleFilter(option.value as JobTitleType);
gaEvent({
action: `offers.table_filter_job_title_${option.value}`,
category: 'engagement',
label: 'Filter by job title',
});
+ } else {
+ setJobTitleFilter('');
}
}}
/>
@@ -76,6 +83,7 @@ export default function OffersHomePage() {
{
if (option) {
setCompanyFilter(option.value);
@@ -84,19 +92,21 @@ export default function OffersHomePage() {
category: 'engagement',
label: 'Filter by company',
});
+ } else {
+ setCompanyFilter('');
}
}}
/>
-
+
-
+
);
}
diff --git a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
index 46190bce..092cf8b9 100644
--- a/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
+++ b/apps/portal/src/pages/offers/profile/[offerProfileId].tsx
@@ -195,24 +195,26 @@ export default function OfferProfile() {
)}
{getProfileQuery.isLoading && (
-
)}
{!getProfileQuery.isLoading && !getProfileQuery.isError && (
-
-
-
-
+
+
-
+
{getAnalysis.isLoading && (
-
)}
{!getAnalysis.isLoading && (
-
-
-
-
+
+
+
+
- {steps[step]}
- {step === 0 && (
-
- setStep(step + 1)}
- />
-
- )}
- {step === 1 && (
-
- setStep(step - 1)}
- />
-
-
- )}
+
+ {steps[step]}
+ {step === 0 && (
+
+ setStep(step + 1)}
+ />
+
+ )}
+ {step === 1 && (
+
+ setStep(step - 1)}
+ />
+
+
+ )}
+
diff --git a/apps/portal/src/pages/offers/test/createProfile.tsx b/apps/portal/src/pages/offers/test/createProfile.tsx
deleted file mode 100644
index 610d4e00..00000000
--- a/apps/portal/src/pages/offers/test/createProfile.tsx
+++ /dev/null
@@ -1,412 +0,0 @@
-import React, { useState } from 'react';
-
-import { trpc } from '~/utils/trpc';
-
-function Test() {
- const [createdData, setCreatedData] = useState('');
- const [error, setError] = useState('');
-
- const createMutation = trpc.useMutation(['offers.profile.create'], {
- onError(err) {
- alert(err);
- },
- onSuccess(data) {
- setCreatedData(JSON.stringify(data));
- },
- });
-
- const addToUserProfileMutation = trpc.useMutation(
- ['offers.user.profile.addToUserProfile'],
- {
- onError(err) {
- alert(err);
- },
- onSuccess(data) {
- setCreatedData(JSON.stringify(data));
- },
- },
- );
-
- const deleteCommentMutation = trpc.useMutation(['offers.comments.delete'], {
- onError(err) {
- alert(err);
- },
- onSuccess(data) {
- setCreatedData(JSON.stringify(data));
- },
- });
-
- const handleDeleteComment = () => {
- deleteCommentMutation.mutate({
- id: 'cl97fprun001j7iyg6ev9x983',
- profileId: 'cl96stky5002ew32gx2kale2x',
- token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
- userId: 'cl97dl51k001e7iygd5v5gt58',
- });
- };
-
- const updateCommentMutation = trpc.useMutation(['offers.comments.update'], {
- onError(err) {
- alert(err);
- },
- onSuccess(data) {
- setCreatedData(JSON.stringify(data));
- },
- });
-
- const handleUpdateComment = () => {
- updateCommentMutation.mutate({
- id: 'cl97fxb0y001l7iyg14sdobt2',
- message: 'hello hello',
- profileId: 'cl96stky5002ew32gx2kale2x',
- token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
- });
- };
-
- const createCommentMutation = trpc.useMutation(['offers.comments.create'], {
- onError(err) {
- alert(err);
- },
- onSuccess(data) {
- setCreatedData(JSON.stringify(data));
- },
- });
-
- const handleCreate = () => {
- createCommentMutation.mutate({
- message: 'wassup bro',
- profileId: 'cl9efyn9p004ww3u42mjgl1vn',
- replyingToId: 'cl9el4xj10001w3w21o3p2iny',
- userId: 'cl9ehvpng0000w3ec2mpx0bdd',
- });
- };
-
- const handleLink = () => {
- addToUserProfileMutation.mutate({
- profileId: 'cl9efyn9p004ww3u42mjgl1vn',
- token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
- // UserId: 'cl9ehvpng0000w3ec2mpx0bdd',
- });
- };
-
- const handleClick = () => {
- createMutation.mutate({
- background: {
- educations: [
- {
- endDate: new Date('2018-09-30T07:58:54.000Z'),
- field: 'Computer Science',
- school: 'National University of Singapore',
- startDate: new Date('2014-09-30T07:58:54.000Z'),
- type: 'Bachelors',
- },
- ],
- experiences: [
- {
- companyId: 'cl9j4yawz0003utlp1uaa1t8o',
- durationInMonths: 24,
- jobType: 'FULLTIME',
- level: 'Junior',
- title: 'software-engineer',
- totalCompensation: {
- currency: 'SGD',
- value: 104100,
- },
- },
- ],
- specificYoes: [
- {
- domain: 'Front End',
- yoe: 2,
- },
- {
- domain: 'Full Stack',
- yoe: 2,
- },
- ],
- totalYoe: 4,
- },
- offers: [
- {
- comments: 'I am a Raffles Institution almumni',
- // Comments: '',
- companyId: 'cl9j4yawz0003utlp1uaa1t8o',
- jobType: 'FULLTIME',
- location: 'Singapore, Singapore',
- monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
- negotiationStrategy: 'Leveraged having multiple offers',
- offersFullTime: {
- baseSalary: {
- currency: 'SGD',
- value: 2222,
- },
- bonus: {
- currency: 'SGD',
- value: 2222,
- },
- level: 'Junior',
- stocks: {
- currency: 'SGD',
- value: 0,
- },
- title: 'software-engineer',
- totalCompensation: {
- currency: 'SGD',
- value: 4444,
- },
- },
- },
- {
- comments: '',
- companyId: 'cl9j4yawz0003utlp1uaa1t8o',
- jobType: 'FULLTIME',
- location: 'Singapore, Singapore',
- monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
- negotiationStrategy: 'Leveraged having multiple offers',
- offersFullTime: {
- baseSalary: {
- currency: 'SGD',
- value: 84000,
- },
- bonus: {
- currency: 'SGD',
- value: 20000,
- },
- level: 'Junior',
- stocks: {
- currency: 'SGD',
- value: 100,
- },
- title: 'software-engineer',
- totalCompensation: {
- currency: 'SGD',
- value: 104100,
- },
- },
- },
- ],
- });
- };
-
- const profileId = 'cl9j50xzk008vutfqg6mta2ey'; // Remember to change this filed after testing deleting
- const data = trpc.useQuery(
- [
- `offers.profile.listOne`,
- {
- profileId,
- token:
- '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
- },
- ],
- {
- onError(err) {
- setError(err.shape?.message || '');
- },
- },
- );
-
- trpc.useQuery(
- [
- `offers.profile.isValidToken`,
- {
- profileId: 'cl9scdzuh0000tt727ipone1k',
- token:
- 'aa628d0db3ad7a5f84895537d4cca38edd0a9b8b96d869cddeb967fccf068c08',
- },
- ],
- {
- onError(err) {
- setError(err.shape?.message || '');
- },
- },
- );
-
- const replies = trpc.useQuery(
- ['offers.comments.getComments', { profileId }],
- {
- onError(err) {
- setError(err.shape?.message || '');
- },
- },
- );
-
- const deleteMutation = trpc.useMutation(['offers.profile.delete']);
-
- const handleDelete = (id: string) => {
- deleteMutation.mutate({
- profileId: id,
- token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
- });
- };
-
- const updateMutation = trpc.useMutation(['offers.profile.update'], {
- onError(err) {
- alert(err);
- },
- onSuccess(response) {
- setCreatedData(JSON.stringify(response));
- },
- });
-
- const handleUpdate = () => {
- updateMutation.mutate({
- background: {
- educations: [
- {
- backgroundId: 'cl9i68fv60001tthj23g9tuv4',
- endDate: new Date('2018-09-30T07:58:54.000Z'),
- field: 'Computer Science',
- id: 'cl9i87y7z004otthjmpsd48wo',
- school: 'National University of Singapore',
- startDate: new Date('2014-09-30T07:58:54.000Z'),
- type: 'Bachelors',
- },
- ],
- experiences: [
- {
- backgroundId: 'cl9i68fv60001tthj23g9tuv4',
- 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',
- durationInMonths: 24,
- // Id: 'cl9j4yawz0003utlp1uaa1t8o',
- jobType: 'FULLTIME',
- level: 'Junior',
- monthlySalary: null,
- monthlySalaryId: null,
- title: 'software-engineer',
- totalCompensation: {
- currency: 'SGD',
- id: 'cl9i68fvc0005tthj7r1rhvb1',
- value: 100,
- },
- totalCompensationId: 'cl9i68fvc0005tthj7r1rhvb1',
- },
- ],
- id: 'cl9i68fv60001tthj23g9tuv4',
- offersProfileId: 'cl9i68fv60000tthj8t3zkox0',
- specificYoes: [
- {
- backgroundId: 'cl9i68fv60001tthj23g9tuv4',
- domain: 'Backend',
- id: 'cl9i68fvc0008tthjlxslzfo4',
- yoe: 5,
- },
- {
- backgroundId: 'cl9i68fv60001tthj23g9tuv4',
- domain: 'Backend',
- id: 'cl9i68fvc0009tthjwol3285l',
- yoe: 4,
- },
- ],
- totalYoe: 1,
- },
- createdAt: '2022-10-13T08:28:13.518Z',
- // Discussion: [],
- id: 'cl9i68fv60000tthj8t3zkox0',
- isEditable: true,
- offers: [
- {
- comments: 'this IS SO IEUHDAEUIGDI',
- 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: 'cl9i68fve000ntthj5h9yvqnh',
- 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: 'cl9i68fve000ptthjn55hpoe4',
- value: 1999999999,
- },
- baseSalaryId: 'cl9i68fve000ptthjn55hpoe4',
- bonus: {
- currency: 'SGD',
- id: 'cl9i68fve000rtthjqo2ktljt',
- value: 1410065407,
- },
- bonusId: 'cl9i68fve000rtthjqo2ktljt',
- id: 'cl9i68fve000otthjqk0g01k0',
- level: 'EXPERT',
- stocks: {
- currency: 'SGD',
- id: 'cl9i68fvf000ttthjt2ode0cc',
- value: -558038585,
- },
- stocksId: 'cl9i68fvf000ttthjt2ode0cc',
- title: 'software-engineer',
- totalCompensation: {
- currency: 'SGD',
- id: 'cl9i68fvf000vtthjg90s48nj',
- value: 55555555,
- },
- totalCompensationId: 'cl9i68fvf000vtthjg90s48nj',
- },
- offersFullTimeId: 'cl9i68fve000otthjqk0g01k0',
- offersIntern: null,
- offersInternId: null,
- profileId: 'cl9i68fv60000tthj8t3zkox0',
- },
- ],
-
- token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
- userId: null,
- });
- };
-
- return (
- <>
-
{createdData}
-
{JSON.stringify(replies.data?.data)}
-
- Click Me!
-
-
- UPDATE!
-
-
- LINKKKK!
-
-
- CREATE COMMENT!
-
-
- DELETE COMMENT!
-
-
- UPDATE COMMENT!
-
-
{
- handleDelete(profileId);
- }}>
- DELETE THIS PROFILE
-
-
{JSON.stringify(data.data)}
-
{JSON.stringify(error)}
- >
- );
-}
-
-export default Test;
diff --git a/apps/portal/src/pages/offers/test/generateAnalysis.tsx b/apps/portal/src/pages/offers/test/generateAnalysis.tsx
deleted file mode 100644
index bf062448..00000000
--- a/apps/portal/src/pages/offers/test/generateAnalysis.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react';
-
-import { trpc } from '~/utils/trpc';
-
-function GenerateAnalysis() {
- const analysisMutation = trpc.useMutation(['offers.analysis.generate']);
-
- return (
-
- {JSON.stringify(
- analysisMutation.mutate({ profileId: 'cl9lwe9m902k5utskjs52wc0j' }),
- )}
-
- );
-}
-
-export default GenerateAnalysis;
diff --git a/apps/portal/src/pages/offers/test/getAnalysis.tsx b/apps/portal/src/pages/offers/test/getAnalysis.tsx
deleted file mode 100644
index 7d3de61e..00000000
--- a/apps/portal/src/pages/offers/test/getAnalysis.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from 'react';
-
-import { trpc } from '~/utils/trpc';
-
-function GetAnalysis() {
- const analysis = trpc.useQuery([
- 'offers.analysis.get',
- { profileId: 'cl9lwe9m902k5utskjs52wc0j' },
- ]);
-
- return
{JSON.stringify(analysis.data)}
;
-}
-
-export default GetAnalysis;
diff --git a/apps/portal/src/pages/offers/test/listOffers.tsx b/apps/portal/src/pages/offers/test/listOffers.tsx
deleted file mode 100644
index a06f0957..00000000
--- a/apps/portal/src/pages/offers/test/listOffers.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React from 'react';
-
-import { trpc } from '~/utils/trpc';
-
-function Test() {
- const data = trpc.useQuery([
- 'offers.list',
- {
- currency: 'SGD',
- limit: 100,
- location: 'Singapore, Singapore',
- offset: 0,
- sortBy: '-totalCompensation',
- yoeCategory: 1,
- },
- ]);
-
- const deleteMutation = trpc.useMutation(['offers.profile.delete']);
-
- const handleDelete = (id: string) => {
- deleteMutation.mutate({ profileId: id, token: ' dadaadad' });
- };
-
- return (
-
-
- {JSON.stringify(data.data?.paging)}
-
-
-
- {data.data?.data.map((offer) => {
- return (
-
- {
- handleDelete(offer.profileId);
- }}>
- DELETE THIS PROFILE AND ALL ITS OFFERS
-
- {JSON.stringify(offer)}
-
-
- );
- })}
-
-
-
- );
-}
-
-export default Test;
diff --git a/apps/portal/src/pages/questions/browse.tsx b/apps/portal/src/pages/questions/browse.tsx
index 9a974f5d..83f25834 100644
--- a/apps/portal/src/pages/questions/browse.tsx
+++ b/apps/portal/src/pages/questions/browse.tsx
@@ -22,6 +22,7 @@ import type { QuestionAge } from '~/utils/questions/constants';
import { APP_TITLE } from '~/utils/questions/constants';
import { QUESTION_AGES, QUESTION_TYPES } from '~/utils/questions/constants';
import createSlug from '~/utils/questions/createSlug';
+import { locationOptionToSlug } from '~/utils/questions/locationSlug';
import relabelQuestionAggregates from '~/utils/questions/relabelQuestionAggregates';
import {
useSearchParam,
@@ -33,20 +34,11 @@ import type { Location } from '~/types/questions.d';
import { SortType } from '~/types/questions.d';
import { SortOrder } from '~/types/questions.d';
-function locationToSlug(value: Location & TypeaheadOption): string {
- return [
- value.countryId,
- value.stateId,
- value.cityId,
- value.id,
- value.label,
- value.value,
- ].join('-');
-}
-
export default function QuestionsBrowsePage() {
const router = useRouter();
+ const [query, setQuery] = useState('');
+
const [
selectedCompanySlugs,
setSelectedCompanySlugs,
@@ -86,7 +78,7 @@ export default function QuestionsBrowsePage() {
useSearchParam('roles');
const [selectedLocations, setSelectedLocations, areLocationsInitialized] =
useSearchParam
('locations', {
- paramToString: locationToSlug,
+ paramToString: locationOptionToSlug,
stringToParam: (param) => {
const [countryId, stateId, cityId, id, label, value] = param.split('-');
return { cityId, countryId, id, label, stateId, value };
@@ -170,13 +162,14 @@ export default function QuestionsBrowsePage() {
const questionsInfiniteQuery = trpc.useInfiniteQuery(
[
- 'questions.questions.getQuestionsByFilter',
+ 'questions.questions.getQuestionsByFilterAndContent',
{
// TODO: Enable filtering by countryIds and stateIds
cityIds: selectedLocations
.map(({ cityId }) => cityId)
.filter((id) => id !== undefined) as Array,
companyIds: selectedCompanySlugs.map((slug) => slug.split('_')[0]),
+ content: query,
countryIds: [],
endDate: today,
limit: 10,
@@ -263,7 +256,7 @@ export default function QuestionsBrowsePage() {
pathname,
query: {
companies: selectedCompanySlugs,
- locations: selectedLocations.map(locationToSlug),
+ locations: selectedLocations.map(locationOptionToSlug),
questionAge: selectedQuestionAge,
questionTypes: selectedQuestionTypes,
roles: selectedRoles,
@@ -351,7 +344,6 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true}
placeholder="Search companies"
onSelect={(option) => {
- // @ts-ignore TODO(questions): handle potentially null value.
onOptionChange({
...option,
checked: true,
@@ -392,7 +384,6 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true}
placeholder="Search roles"
onSelect={(option) => {
- // @ts-ignore TODO(questions): handle potentially null value.
onOptionChange({
...option,
checked: true,
@@ -453,7 +444,6 @@ export default function QuestionsBrowsePage() {
isLabelHidden={true}
placeholder="Search locations"
onSelect={(option) => {
- // @ts-ignore TODO(offers): fix potentially empty value.
onOptionChange({
...option,
checked: true,
@@ -485,8 +475,8 @@ export default function QuestionsBrowsePage() {
-
-
+
+
{
const { cityId, countryId, stateId } = data.location;
@@ -505,11 +495,15 @@ export default function QuestionsBrowsePage() {
{
setFilterDrawerOpen(!filterDrawerOpen);
}}
+ onQueryChange={(newQuery) => {
+ setQuery(newQuery);
+ }}
onSortOrderChange={setSortOrder}
onSortTypeChange={setSortType}
/>
diff --git a/apps/portal/src/pages/questions/index.tsx b/apps/portal/src/pages/questions/index.tsx
index 0f5f07f1..37999946 100644
--- a/apps/portal/src/pages/questions/index.tsx
+++ b/apps/portal/src/pages/questions/index.tsx
@@ -10,13 +10,13 @@ export default function QuestionsHomePage() {
const router = useRouter();
const handleLandingQuery = async (data: LandingQueryData) => {
- const { company, location, questionType } = data;
+ const { companySlug, location, questionType } = data;
// Go to browse page
router.push({
pathname: '/questions/browse',
query: {
- companies: [company],
+ companies: [companySlug],
locations: [location],
questionTypes: [questionType],
},
diff --git a/apps/portal/src/pages/resumes/[resumeId].tsx b/apps/portal/src/pages/resumes/[resumeId].tsx
index 3c0604b4..4e6c9bea 100644
--- a/apps/portal/src/pages/resumes/[resumeId].tsx
+++ b/apps/portal/src/pages/resumes/[resumeId].tsx
@@ -22,6 +22,7 @@ import ResumeCommentsForm from '~/components/resumes/comments/ResumeCommentsForm
import ResumeCommentsList from '~/components/resumes/comments/ResumeCommentsList';
import ResumePdf from '~/components/resumes/ResumePdf';
import ResumeExpandableText from '~/components/resumes/shared/ResumeExpandableText';
+import loginPageHref from '~/components/shared/loginPageHref';
import type {
ExperienceFilter,
@@ -107,7 +108,7 @@ export default function ResumeReviewPage() {
const onStarButtonClick = () => {
if (session?.user?.id == null) {
- router.push('/api/auth/signin');
+ router.push(loginPageHref());
return;
}
@@ -184,8 +185,8 @@ export default function ResumeReviewPage() {
);
diff --git a/apps/portal/src/pages/resumes/index.tsx b/apps/portal/src/pages/resumes/index.tsx
index 9a0ca902..549a7849 100644
--- a/apps/portal/src/pages/resumes/index.tsx
+++ b/apps/portal/src/pages/resumes/index.tsx
@@ -24,6 +24,7 @@ import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import ResumeFilterPill from '~/components/resumes/browse/ResumeFilterPill';
import ResumeListItems from '~/components/resumes/browse/ResumeListItems';
import ResumeSignInButton from '~/components/resumes/shared/ResumeSignInButton';
+import loginPageHref from '~/components/shared/loginPageHref';
import type {
Filter,
@@ -257,7 +258,7 @@ export default function ResumeHomePage() {
const onSubmitResume = () => {
if (sessionData === null) {
- router.push('/api/auth/signin');
+ router.push(loginPageHref());
} else {
router.push('/resumes/submit');
}
diff --git a/apps/portal/src/pages/resumes/submit.tsx b/apps/portal/src/pages/resumes/submit.tsx
index 7ef404dc..8aefe28b 100644
--- a/apps/portal/src/pages/resumes/submit.tsx
+++ b/apps/portal/src/pages/resumes/submit.tsx
@@ -21,6 +21,7 @@ import {
import { useGoogleAnalytics } from '~/components/global/GoogleAnalytics';
import SubmissionGuidelines from '~/components/resumes/submit-form/SubmissionGuidelines';
+import loginPageHref from '~/components/shared/loginPageHref';
import { RESUME_STORAGE_KEY } from '~/constants/file-storage-keys';
import { EXPERIENCES, LOCATIONS, ROLES } from '~/utils/resumes/resumeFilters';
@@ -129,7 +130,7 @@ export default function SubmitResumeForm({
// Route user to sign in if not logged in
useEffect(() => {
if (status === 'unauthenticated') {
- router.push('/api/auth/signin');
+ router.push(loginPageHref());
}
}, [router, status]);
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 8617da46..3899fb97 100644
--- a/apps/portal/src/server/router/offers/offers-profile-router.ts
+++ b/apps/portal/src/server/router/offers/offers-profile-router.ts
@@ -425,8 +425,8 @@ export const offersProfileRouter = createRouter()
level: x.level,
location: {
connect: {
- id: x.cityId
- }
+ id: x.cityId,
+ },
},
title: x.title,
totalCompensation:
@@ -481,9 +481,9 @@ export const offersProfileRouter = createRouter()
location: {
connect: {
where: {
- id: x.cityId
- }
- }
+ id: x.cityId,
+ },
+ },
},
title: x.title,
totalCompensation:
@@ -539,9 +539,9 @@ export const offersProfileRouter = createRouter()
location: {
connect: {
where: {
- id: x.cityId
- }
- }
+ id: x.cityId,
+ },
+ },
},
monthlySalary:
x.monthlySalary != null
@@ -595,9 +595,9 @@ export const offersProfileRouter = createRouter()
location: {
connect: {
where: {
- id: x.cityId
- }
- }
+ id: x.cityId,
+ },
+ },
},
monthlySalary:
x.monthlySalary != null
@@ -680,10 +680,8 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType,
location: {
connect: {
- where: {
- id: x.cityId
- }
- }
+ id: x.cityId,
+ },
},
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy,
@@ -726,10 +724,8 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType,
location: {
connect: {
- where: {
- id: x.cityId
- }
- }
+ id: x.cityId,
+ },
},
monthYearReceived: x.monthYearReceived,
negotiationStrategy: x.negotiationStrategy,
@@ -988,26 +984,10 @@ export const offersProfileRouter = createRouter()
});
if (exp.monthlySalary) {
- if (exp.monthlySalary.id) {
- await ctx.prisma.offersCurrency.update({
- data: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- exp.monthlySalary.value,
- exp.monthlySalary.currency,
- baseCurrencyString,
- ),
- currency: exp.monthlySalary.currency,
- value: exp.monthlySalary.value,
- },
- where: {
- id: exp.monthlySalary.id,
- },
- });
- } else {
- await ctx.prisma.offersExperience.update({
- data: {
- monthlySalary: {
+ await ctx.prisma.offersExperience.update({
+ data: {
+ monthlySalary: {
+ upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
@@ -1018,36 +998,30 @@ export const offersProfileRouter = createRouter()
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
+ update: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ exp.monthlySalary.value,
+ exp.monthlySalary.currency,
+ baseCurrencyString,
+ ),
+ currency: exp.monthlySalary.currency,
+ value: exp.monthlySalary.value,
+ },
},
},
- where: {
- id: exp.id,
- },
- });
- }
+ },
+ where: {
+ id: exp.id,
+ },
+ });
}
if (exp.totalCompensation) {
- if (exp.totalCompensation.id) {
- await ctx.prisma.offersCurrency.update({
- data: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- exp.totalCompensation.value,
- exp.totalCompensation.currency,
- baseCurrencyString,
- ),
- currency: exp.totalCompensation.currency,
- value: exp.totalCompensation.value,
- },
- where: {
- id: exp.totalCompensation.id,
- },
- });
- } else {
- await ctx.prisma.offersExperience.update({
- data: {
- totalCompensation: {
+ await ctx.prisma.offersExperience.update({
+ data: {
+ totalCompensation: {
+ upsert: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
@@ -1058,13 +1032,23 @@ export const offersProfileRouter = createRouter()
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
+ update: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ exp.totalCompensation.value,
+ exp.totalCompensation.currency,
+ baseCurrencyString,
+ ),
+ currency: exp.totalCompensation.currency,
+ value: exp.totalCompensation.value,
+ },
},
},
- where: {
- id: exp.id,
- },
- });
- }
+ },
+ where: {
+ id: exp.id,
+ },
+ });
}
} else if (!exp.id) {
// Create new experience
@@ -1090,8 +1074,8 @@ export const offersProfileRouter = createRouter()
level: exp.level,
location: {
connect: {
- id: exp.cityId
- }
+ id: exp.cityId,
+ },
},
title: exp.title,
totalCompensation: exp.totalCompensation
@@ -1161,8 +1145,8 @@ export const offersProfileRouter = createRouter()
level: exp.level,
location: {
connect: {
- id: exp.cityId
- }
+ id: exp.cityId,
+ },
},
title: exp.title,
totalCompensation: {
@@ -1229,8 +1213,8 @@ export const offersProfileRouter = createRouter()
level: exp.level,
location: {
connect: {
- id: exp.cityId
- }
+ id: exp.cityId,
+ },
},
title: exp.title,
},
@@ -1272,8 +1256,8 @@ export const offersProfileRouter = createRouter()
level: exp.level,
location: {
connect: {
- id: exp.cityId
- }
+ id: exp.cityId,
+ },
},
title: exp.title,
},
@@ -1321,8 +1305,8 @@ export const offersProfileRouter = createRouter()
jobType: exp.jobType,
location: {
connect: {
- id: exp.cityId
- }
+ id: exp.cityId,
+ },
},
monthlySalary: {
create: {
@@ -1386,8 +1370,8 @@ export const offersProfileRouter = createRouter()
jobType: exp.jobType,
location: {
connect: {
- id: exp.cityId
- }
+ id: exp.cityId,
+ },
},
monthlySalary: {
create: {
@@ -1453,7 +1437,7 @@ export const offersProfileRouter = createRouter()
location: {
connect: {
id: exp.cityId,
- }
+ },
},
title: exp.title,
},
@@ -1493,8 +1477,8 @@ export const offersProfileRouter = createRouter()
jobType: exp.jobType,
location: {
connect: {
- id: exp.cityId
- }
+ id: exp.cityId,
+ },
},
title: exp.title,
},
@@ -1600,8 +1584,8 @@ export const offersProfileRouter = createRouter()
comments: offerToUpdate.comments,
company: {
connect: {
- id: offerToUpdate.companyId
- }
+ id: offerToUpdate.companyId,
+ },
},
jobType:
offerToUpdate.jobType === JobType.FULLTIME
@@ -1609,8 +1593,8 @@ export const offersProfileRouter = createRouter()
: JobType.INTERN,
location: {
connect: {
- id: offerToUpdate.cityId
- }
+ id: offerToUpdate.cityId,
+ },
},
monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy,
@@ -1625,6 +1609,32 @@ export const offersProfileRouter = createRouter()
data: {
internshipCycle:
offerToUpdate.offersIntern.internshipCycle ?? undefined,
+ monthlySalary: {
+ upsert: {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersIntern.monthlySalary.value,
+ offerToUpdate.offersIntern.monthlySalary.currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersIntern.monthlySalary.currency,
+ value: offerToUpdate.offersIntern.monthlySalary.value,
+ },
+ update: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersIntern.monthlySalary.value,
+ offerToUpdate.offersIntern.monthlySalary.currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersIntern.monthlySalary.currency,
+ value: offerToUpdate.offersIntern.monthlySalary.value,
+ },
+ },
+ },
startYear: offerToUpdate.offersIntern.startYear ?? undefined,
title: offerToUpdate.offersIntern.title,
},
@@ -1632,21 +1642,6 @@ export const offersProfileRouter = createRouter()
id: offerToUpdate.offersIntern.id,
},
});
- await ctx.prisma.offersCurrency.update({
- data: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- offerToUpdate.offersIntern.monthlySalary.value,
- offerToUpdate.offersIntern.monthlySalary.currency,
- baseCurrencyString,
- ),
- currency: offerToUpdate.offersIntern.monthlySalary.currency,
- value: offerToUpdate.offersIntern.monthlySalary.value,
- },
- where: {
- id: offerToUpdate.offersIntern.monthlySalary.id,
- },
- });
}
if (offerToUpdate.offersFullTime?.totalCompensation != null) {
@@ -1660,70 +1655,145 @@ export const offersProfileRouter = createRouter()
},
});
if (offerToUpdate.offersFullTime.baseSalary != null) {
- await ctx.prisma.offersCurrency.update({
+ await ctx.prisma.offersFullTime.update({
data: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- offerToUpdate.offersFullTime.baseSalary.value,
- offerToUpdate.offersFullTime.baseSalary.currency,
- baseCurrencyString,
- ),
- currency: offerToUpdate.offersFullTime.baseSalary.currency,
- value: offerToUpdate.offersFullTime.baseSalary.value,
+ baseSalary: {
+ upsert: {
+ 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,
+ },
+ update: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.baseSalary.value,
+ offerToUpdate.offersFullTime.baseSalary.currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersFullTime.baseSalary.currency,
+ value: offerToUpdate.offersFullTime.baseSalary.value,
+ },
+ },
+ },
},
where: {
- id: offerToUpdate.offersFullTime.baseSalary.id,
+ id: offerToUpdate.offersFullTime.id,
},
});
}
if (offerToUpdate.offersFullTime.bonus != null) {
- await ctx.prisma.offersCurrency.update({
+ await ctx.prisma.offersFullTime.update({
data: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- offerToUpdate.offersFullTime.bonus.value,
- offerToUpdate.offersFullTime.bonus.currency,
- baseCurrencyString,
- ),
- currency: offerToUpdate.offersFullTime.bonus.currency,
- value: offerToUpdate.offersFullTime.bonus.value,
+ bonus: {
+ upsert: {
+ 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,
+ },
+ update: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.bonus.value,
+ offerToUpdate.offersFullTime.bonus.currency,
+ baseCurrencyString,
+ ),
+ currency: offerToUpdate.offersFullTime.bonus.currency,
+ value: offerToUpdate.offersFullTime.bonus.value,
+ },
+ },
+ },
},
where: {
- id: offerToUpdate.offersFullTime.bonus.id,
+ id: offerToUpdate.offersFullTime.id,
},
});
}
if (offerToUpdate.offersFullTime.stocks != null) {
- await ctx.prisma.offersCurrency.update({
+ await ctx.prisma.offersFullTime.update({
data: {
- 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: {
+ upsert: {
+ 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,
+ },
+ update: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.stocks.value,
+ offerToUpdate.offersFullTime.stocks.currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersFullTime.stocks.currency,
+ value: offerToUpdate.offersFullTime.stocks.value,
+ },
+ },
+ },
},
where: {
- id: offerToUpdate.offersFullTime.stocks.id,
+ id: offerToUpdate.offersFullTime.id,
},
});
}
- await ctx.prisma.offersCurrency.update({
+ await ctx.prisma.offersFullTime.update({
data: {
- baseCurrency: baseCurrencyString,
- baseValue: await convert(
- offerToUpdate.offersFullTime.totalCompensation.value,
- offerToUpdate.offersFullTime.totalCompensation.currency,
- baseCurrencyString,
- ),
- currency:
- offerToUpdate.offersFullTime.totalCompensation.currency,
- value: offerToUpdate.offersFullTime.totalCompensation.value,
+ totalCompensation: {
+ upsert: {
+ create: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.totalCompensation.value,
+ offerToUpdate.offersFullTime.totalCompensation
+ .currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersFullTime.totalCompensation
+ .currency,
+ value:
+ offerToUpdate.offersFullTime.totalCompensation.value,
+ },
+ update: {
+ baseCurrency: baseCurrencyString,
+ baseValue: await convert(
+ offerToUpdate.offersFullTime.totalCompensation.value,
+ offerToUpdate.offersFullTime.totalCompensation
+ .currency,
+ baseCurrencyString,
+ ),
+ currency:
+ offerToUpdate.offersFullTime.totalCompensation
+ .currency,
+ value:
+ offerToUpdate.offersFullTime.totalCompensation.value,
+ },
+ },
+ },
},
where: {
- id: offerToUpdate.offersFullTime.totalCompensation.id,
+ id: offerToUpdate.offersFullTime.id,
},
});
}
@@ -1750,8 +1820,8 @@ export const offersProfileRouter = createRouter()
jobType: offerToUpdate.jobType,
location: {
connect: {
- id: offerToUpdate.cityId
- }
+ id: offerToUpdate.cityId,
+ },
},
monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy,
@@ -1808,8 +1878,8 @@ export const offersProfileRouter = createRouter()
jobType: offerToUpdate.jobType,
location: {
connect: {
- id: offerToUpdate.cityId
- }
+ id: offerToUpdate.cityId,
+ },
},
monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy,
diff --git a/apps/portal/src/server/router/offers/offers.ts b/apps/portal/src/server/router/offers/offers.ts
index 916dcb57..5be7f832 100644
--- a/apps/portal/src/server/router/offers/offers.ts
+++ b/apps/portal/src/server/router/offers/offers.ts
@@ -1,4 +1,5 @@
import { z } from 'zod';
+import { JobType } from '@prisma/client';
import { TRPCError } from '@trpc/server';
import {
@@ -12,10 +13,7 @@ import { createValidationRegex } from '~/utils/offers/zodRegex';
import { createRouter } from '../context';
const getOrder = (prefix: string) => {
- if (prefix === '+') {
- return 'asc';
- }
- return 'desc';
+ return prefix === '+' ? 'asc' : 'desc';
};
const sortingKeysMap = {
@@ -31,8 +29,10 @@ const yoeCategoryMap: Record = {
3: 'Senior',
};
-const getYoeRange = (yoeCategory: number) => {
- return yoeCategoryMap[yoeCategory] === 'Fresh Grad'
+const getYoeRange = (yoeCategory: number | null | undefined) => {
+ return yoeCategory == null
+ ? { maxYoe: 100, minYoe: 0 }
+ : yoeCategoryMap[yoeCategory] === 'Fresh Grad'
? { maxYoe: 2, minYoe: 0 }
: yoeCategoryMap[yoeCategory] === 'Mid'
? { maxYoe: 5, minYoe: 3 }
@@ -43,8 +43,8 @@ const getYoeRange = (yoeCategory: number) => {
export const offersRouter = createRouter().query('list', {
input: z.object({
- cityId: z.string(),
companyId: z.string().nullish(),
+ countryId: z.string().nullish(),
currency: z.string().nullish(),
dateEnd: z.date().nullish(),
dateStart: z.date().nullish(),
@@ -57,14 +57,14 @@ export const offersRouter = createRouter().query('list', {
.regex(createValidationRegex(Object.keys(sortingKeysMap), '[+-]{1}'))
.nullish(),
title: z.string().nullish(),
- yoeCategory: z.number().min(0).max(3),
+ yoeCategory: z.number().min(0).max(3).nullish(),
yoeMax: z.number().max(100).nullish(),
yoeMin: z.number().min(0).nullish(),
}),
async resolve({ ctx, input }) {
const yoeRange = getYoeRange(input.yoeCategory);
- const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe;
- const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe;
+ const yoeMin = input.yoeMin != null ? input.yoeMin : yoeRange?.minYoe;
+ const yoeMax = input.yoeMax != null ? input.yoeMax : yoeRange?.maxYoe;
if (!input.sortBy) {
input.sortBy = '-' + sortingKeysMap.monthYearReceived;
@@ -132,7 +132,14 @@ export const offersRouter = createRouter().query('list', {
where: {
AND: [
{
- cityId: input.cityId.length === 0 ? undefined : input.cityId,
+ location: {
+ state: {
+ countryId:
+ input.countryId != null && input.countryId.length !== 0
+ ? input.countryId
+ : undefined,
+ },
+ },
},
{
offersIntern: {
@@ -142,7 +149,7 @@ export const offersRouter = createRouter().query('list', {
{
offersIntern: {
title:
- input.title && input.title.length !== 0
+ input.title != null && input.title.length !== 0
? input.title
: undefined,
},
@@ -245,7 +252,14 @@ export const offersRouter = createRouter().query('list', {
where: {
AND: [
{
- cityId: input.cityId.length === 0 ? undefined : input.cityId,
+ location: {
+ state: {
+ countryId:
+ input.countryId != null && input.countryId.length !== 0
+ ? input.countryId
+ : undefined,
+ },
+ },
},
{
offersIntern: {
@@ -260,7 +274,7 @@ export const offersRouter = createRouter().query('list', {
{
offersFullTime: {
title:
- input.title && input.title.length !== 0
+ input.title != null && input.title.length !== 0
? input.title
: undefined,
},
@@ -380,6 +394,7 @@ export const offersRouter = createRouter().query('list', {
numOfPages: Math.ceil(data.length / input.limit),
totalItems: data.length,
},
+ !yoeRange ? JobType.INTERN : JobType.FULLTIME,
);
},
});
diff --git a/apps/portal/src/server/router/questions/questions-question-router.ts b/apps/portal/src/server/router/questions/questions-question-router.ts
index a5c06f2d..4d506b94 100644
--- a/apps/portal/src/server/router/questions/questions-question-router.ts
+++ b/apps/portal/src/server/router/questions/questions-question-router.ts
@@ -13,6 +13,7 @@ export const questionsQuestionRouter = createRouter()
input: z.object({
cityIds: z.string().array(),
companyIds: z.string().array(),
+ content: z.string().optional(),
countryIds: z.string().array(),
cursor: z.string().nullish(),
endDate: z.date().default(new Date()),
@@ -235,7 +236,8 @@ export const questionsQuestionRouter = createRouter()
SELECT id FROM "QuestionsQuestion"
WHERE
to_tsvector("content") @@ to_tsquery('english', ${query})
- ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC;
+ ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC
+ LIMIT 3;
`;
const relatedQuestionsIdArray = relatedQuestionsId.map(
@@ -280,4 +282,183 @@ export const questionsQuestionRouter = createRouter()
return processedQuestionsData;
},
+ })
+ .query('getQuestionsByFilterAndContent', {
+ input: z.object({
+ cityIds: z.string().array(),
+ companyIds: z.string().array(),
+ content: z.string(),
+ countryIds: z.string().array(),
+ cursor: z.string().nullish(),
+ endDate: z.date().default(new Date()),
+ limit: z.number().min(1).default(50),
+ questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
+ roles: z.string().array(),
+ sortOrder: z.nativeEnum(SortOrder),
+ sortType: z.nativeEnum(SortType),
+ startDate: z.date().optional(),
+ stateIds: z.string().array(),
+ }),
+ async resolve({ ctx, input }) {
+ const escapeChars = /[()|&:*!]/g;
+
+ const query = input.content
+ .replace(escapeChars, ' ')
+ .trim()
+ .split(/\s+/)
+ .join(' | ');
+
+ let relatedQuestionsId: Array<{ id: string }> = [];
+
+ if (input.content !== "") {
+ relatedQuestionsId = await ctx.prisma
+ .$queryRaw`
+ SELECT id FROM "QuestionsQuestion"
+ WHERE
+ to_tsvector("content") @@ to_tsquery('english', ${query})
+ ORDER BY ts_rank_cd(to_tsvector("content"), to_tsquery('english', ${query}), 4) DESC
+ LIMIT 3;
+ `;
+ }
+
+
+
+ const relatedQuestionsIdArray = relatedQuestionsId.map(
+ (current) => current.id,
+ );
+
+ const { cursor } = input;
+
+ const sortCondition =
+ input.sortType === SortType.TOP
+ ? [
+ {
+ upvotes: input.sortOrder,
+ },
+ {
+ id: input.sortOrder,
+ },
+ ]
+ : [
+ {
+ lastSeenAt: input.sortOrder,
+ },
+ {
+ id: input.sortOrder,
+ },
+ ];
+
+ const questionsData = await ctx.prisma.questionsQuestion.findMany({
+ cursor: cursor ? { id: cursor } : undefined,
+ include: {
+ _count: {
+ select: {
+ answers: true,
+ comments: true,
+ },
+ },
+ encounters: {
+ select: {
+ city: true,
+ company: true,
+ country: true,
+ role: true,
+ seenAt: true,
+ state: true,
+ },
+ },
+ user: {
+ select: {
+ name: true,
+ },
+ },
+ votes: true,
+ },
+ orderBy: sortCondition,
+ take: input.limit + 1,
+ where: {
+ id: input.content !== "" ? {
+ in: relatedQuestionsIdArray,
+ } : undefined,
+ ...(input.questionTypes.length > 0
+ ? {
+ questionType: {
+ in: input.questionTypes,
+ },
+ }
+ : {}),
+ encounters: {
+ some: {
+ seenAt: {
+ gte: input.startDate,
+ lte: input.endDate,
+ },
+ ...(input.companyIds.length > 0
+ ? {
+ company: {
+ id: {
+ in: input.companyIds,
+ },
+ },
+ }
+ : {}),
+ ...(input.cityIds.length > 0
+ ? {
+ city: {
+ id: {
+ in: input.cityIds,
+ },
+ },
+ }
+ : {}),
+ ...(input.countryIds.length > 0
+ ? {
+ country: {
+ id: {
+ in: input.countryIds,
+ },
+ },
+ }
+ : {}),
+ ...(input.stateIds.length > 0
+ ? {
+ state: {
+ id: {
+ in: input.stateIds,
+ },
+ },
+ }
+ : {}),
+ ...(input.roles.length > 0
+ ? {
+ role: {
+ in: input.roles,
+ },
+ }
+ : {}),
+ },
+ },
+ },
+ });
+
+ const processedQuestionsData = questionsData.map(
+ createQuestionWithAggregateData,
+ );
+
+ let nextCursor: typeof cursor | undefined = undefined;
+
+ if (questionsData.length > input.limit) {
+ const nextItem = questionsData.pop()!;
+ processedQuestionsData.pop();
+
+ const nextIdCursor: string | undefined = nextItem.id;
+
+ nextCursor = nextIdCursor;
+ }
+
+ return {
+ data: processedQuestionsData,
+ nextCursor,
+ };
+ },
});
diff --git a/apps/portal/src/types/offers.d.ts b/apps/portal/src/types/offers.d.ts
index 3dcba938..425d654d 100644
--- a/apps/portal/src/types/offers.d.ts
+++ b/apps/portal/src/types/offers.d.ts
@@ -65,11 +65,14 @@ export type SpecificYoe = {
};
export type DashboardOffer = {
+ baseSalary?: Valuation;
+ bonus?: Valuation;
company: OffersCompany;
id: string;
income: Valuation;
monthYearReceived: Date;
profileId: string;
+ stocks?: Valuation;
title: string;
totalYoe: number;
};
@@ -123,6 +126,7 @@ export type User = {
export type GetOffersResponse = {
data: Array;
+ jobType: JobType;
paging: Paging;
};
diff --git a/apps/portal/src/utils/offers/analysisGeneration.ts b/apps/portal/src/utils/offers/analysisGeneration.ts
index d9221a08..a2594079 100644
--- a/apps/portal/src/utils/offers/analysisGeneration.ts
+++ b/apps/portal/src/utils/offers/analysisGeneration.ts
@@ -69,122 +69,120 @@ export const generateAnalysis = async (params: {
}) => {
const { ctx, input } = params;
await ctx.prisma.offersAnalysis.deleteMany({
- where: {
- profileId: input.profileId,
- },
- });
+ where: {
+ profileId: input.profileId,
+ },
+ });
- const offers = await ctx.prisma.offersOffer.findMany({
+ const offers = await ctx.prisma.offersOffer.findMany({
+ include: {
+ company: true,
+ location: {
include: {
- company: true,
- location: {
- include: {
- state: {
- include: {
- country: true,
- },
- },
- },
- },
- offersFullTime: {
- include: {
- baseSalary: true,
- bonus: true,
- stocks: true,
- totalCompensation: true,
- },
- },
- offersIntern: {
- include: {
- monthlySalary: true,
- },
- },
- profile: {
+ state: {
include: {
- background: true,
+ country: true,
},
},
},
- orderBy: [
- {
- offersFullTime: {
- totalCompensation: {
- baseValue: 'desc',
- },
- },
+ },
+ offersFullTime: {
+ include: {
+ baseSalary: true,
+ bonus: true,
+ stocks: true,
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: true,
+ },
+ },
+ },
+ orderBy: [
+ {
+ offersFullTime: {
+ totalCompensation: {
+ baseValue: 'desc',
},
- {
- offersIntern: {
- monthlySalary: {
- baseValue: 'desc',
- },
- },
+ },
+ },
+ {
+ offersIntern: {
+ monthlySalary: {
+ baseValue: 'desc',
},
- ],
- where: {
- profileId: input.profileId,
},
- });
+ },
+ ],
+ where: {
+ profileId: input.profileId,
+ },
+ });
- if (!offers || offers.length === 0) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'No offers found on this profile',
- });
- }
+ if (!offers || offers.length === 0) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'No offers found on this profile',
+ });
+ }
- const overallHighestOffer = offers[0];
+ const overallHighestOffer = offers[0];
- if (
- !overallHighestOffer.profile.background ||
- overallHighestOffer.profile.background.totalYoe == null
- ) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'YOE not found',
- });
- }
+ if (
+ !overallHighestOffer.profile.background ||
+ overallHighestOffer.profile.background.totalYoe == null
+ ) {
+ throw new TRPCError({
+ code: 'NOT_FOUND',
+ message: 'YOE not found',
+ });
+ }
- const yoe = overallHighestOffer.profile.background.totalYoe as number;
- const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
- monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
+ const yoe = overallHighestOffer.profile.background.totalYoe as number;
+ const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
+ monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
- let similarOffers = await ctx.prisma.offersOffer.findMany({
+ let similarOffers = await ctx.prisma.offersOffer.findMany({
+ include: {
+ company: true,
+ location: {
include: {
- company: true,
- location: {
- include: {
- state: {
- include: {
- country: true,
- },
- },
- },
- },
- offersFullTime: {
- include: {
- totalCompensation: true,
- },
- },
- offersIntern: {
+ state: {
include: {
- monthlySalary: true,
+ country: true,
},
},
- profile: {
+ },
+ },
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
include: {
- background: {
+ experiences: {
include: {
- experiences: {
+ company: true,
+ location: {
include: {
- company: true,
- location: {
+ state: {
include: {
- state: {
- include: {
- country: true,
- },
- },
+ country: true,
},
},
},
@@ -194,264 +192,214 @@ export const generateAnalysis = async (params: {
},
},
},
- orderBy: [
- {
- offersFullTime: {
- totalCompensation: {
- baseValue: 'desc',
- },
- },
+ },
+ },
+ orderBy: [
+ {
+ offersFullTime: {
+ totalCompensation: {
+ baseValue: 'desc',
},
- {
- offersIntern: {
- monthlySalary: {
- baseValue: 'desc',
- },
- },
+ },
+ },
+ {
+ offersIntern: {
+ monthlySalary: {
+ baseValue: 'desc',
},
- ],
- where: {
- AND: [
- {
- location: overallHighestOffer.location,
- },
+ },
+ },
+ ],
+ where: {
+ AND: [
+ {
+ location: overallHighestOffer.location,
+ },
+ {
+ monthYearReceived: {
+ gte: monthYearReceived,
+ },
+ },
+ {
+ OR: [
{
- monthYearReceived: {
- gte: monthYearReceived,
+ offersFullTime: {
+ title: overallHighestOffer.offersFullTime?.title,
+ },
+ offersIntern: {
+ title: overallHighestOffer.offersIntern?.title,
},
},
- {
- OR: [
+ ],
+ },
+ {
+ profile: {
+ background: {
+ AND: [
{
- offersFullTime: {
- title: overallHighestOffer.offersFullTime?.title,
- },
- offersIntern: {
- title: overallHighestOffer.offersIntern?.title,
+ totalYoe: {
+ gte: Math.max(yoe - 1, 0),
+ lte: yoe + 1,
},
},
],
},
- {
- profile: {
- background: {
- AND: [
- {
- totalYoe: {
- gte: Math.max(yoe - 1, 0),
- lte: yoe + 1,
- },
- },
- ],
- },
- },
- },
- ],
+ },
},
- });
-
- // COMPANY ANALYSIS
- const companyMap = new Map();
- offers.forEach((offer) => {
- if (companyMap.get(offer.companyId) == null) {
- companyMap.set(offer.companyId, offer);
- }
- });
-
- const companyAnalysis = Array.from(companyMap.values()).map(
- (companyOffer) => {
- // TODO: Refactor calculating analysis into a function
- let similarCompanyOffers = similarOffers.filter(
- (offer) => offer.companyId === companyOffer.companyId,
- );
-
- const companyIndex = searchOfferPercentile(
- companyOffer,
- similarCompanyOffers,
- );
- const companyPercentile =
- similarCompanyOffers.length <= 1
- ? 100
- : 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
+ ],
+ },
+ });
- // Get top offers (excluding user's offer)
- similarCompanyOffers = similarCompanyOffers.filter(
- (offer) => offer.id !== companyOffer.id,
- );
-
- const noOfSimilarCompanyOffers = similarCompanyOffers.length;
- const similarCompanyOffers90PercentileIndex = Math.ceil(
- noOfSimilarCompanyOffers * 0.1,
- );
- const topPercentileCompanyOffers =
- noOfSimilarCompanyOffers > 2
- ? similarCompanyOffers.slice(
- similarCompanyOffers90PercentileIndex,
- similarCompanyOffers90PercentileIndex + 2,
- )
- : similarCompanyOffers;
+ // COMPANY ANALYSIS
+ const companyMap = new Map();
+ offers.forEach((offer) => {
+ if (companyMap.get(offer.companyId) == null) {
+ companyMap.set(offer.companyId, offer);
+ }
+ });
- return {
- companyName: companyOffer.company.name,
- noOfSimilarOffers: noOfSimilarCompanyOffers,
- percentile: companyPercentile,
- topSimilarOffers: topPercentileCompanyOffers,
- };
- },
+ const companyAnalysis = Array.from(companyMap.values()).map(
+ (companyOffer) => {
+ // TODO: Refactor calculating analysis into a function
+ let similarCompanyOffers = similarOffers.filter(
+ (offer) => offer.companyId === companyOffer.companyId,
);
- // OVERALL ANALYSIS
- const overallIndex = searchOfferPercentile(
- overallHighestOffer,
- similarOffers,
+ const companyIndex = searchOfferPercentile(
+ companyOffer,
+ similarCompanyOffers,
);
- const overallPercentile =
- similarOffers.length <= 1
+ const companyPercentile =
+ similarCompanyOffers.length <= 1
? 100
- : 100 - (100 * overallIndex) / (similarOffers.length - 1);
+ : 100 - (100 * companyIndex) / (similarCompanyOffers.length - 1);
- similarOffers = similarOffers.filter(
- (offer) => offer.id !== overallHighestOffer.id,
+ // Get top offers (excluding user's offer)
+ similarCompanyOffers = similarCompanyOffers.filter(
+ (offer) => offer.id !== companyOffer.id,
);
- const noOfSimilarOffers = similarOffers.length;
- const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
- const topPercentileOffers =
- noOfSimilarOffers > 2
- ? similarOffers.slice(
- similarOffers90PercentileIndex,
- similarOffers90PercentileIndex + 2,
+ const noOfSimilarCompanyOffers = similarCompanyOffers.length;
+ const similarCompanyOffers90PercentileIndex = Math.ceil(
+ noOfSimilarCompanyOffers * 0.1,
+ );
+ const topPercentileCompanyOffers =
+ noOfSimilarCompanyOffers > 2
+ ? similarCompanyOffers.slice(
+ similarCompanyOffers90PercentileIndex,
+ similarCompanyOffers90PercentileIndex + 2,
)
- : similarOffers;
+ : similarCompanyOffers;
- const analysis = await ctx.prisma.offersAnalysis.create({
- data: {
- companyAnalysis: {
- create: companyAnalysis.map((analysisUnit) => {
- return {
- companyName: analysisUnit.companyName,
- noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
- percentile: analysisUnit.percentile,
- topSimilarOffers: {
- connect: analysisUnit.topSimilarOffers.map((offer) => {
- return { id: offer.id };
- }),
- },
- };
- }),
- },
- overallAnalysis: {
- create: {
- companyName: overallHighestOffer.company.name,
- noOfSimilarOffers,
- percentile: overallPercentile,
- topSimilarOffers: {
- connect: topPercentileOffers.map((offer) => {
- return { id: offer.id };
- }),
- },
- },
- },
- overallHighestOffer: {
- connect: {
- id: overallHighestOffer.id,
- },
- },
- profile: {
- connect: {
- id: input.profileId,
+ return {
+ companyName: companyOffer.company.name,
+ noOfSimilarOffers: noOfSimilarCompanyOffers,
+ percentile: companyPercentile,
+ topSimilarOffers: topPercentileCompanyOffers,
+ };
+ },
+ );
+
+ // OVERALL ANALYSIS
+ const overallIndex = searchOfferPercentile(
+ overallHighestOffer,
+ similarOffers,
+ );
+ const overallPercentile =
+ similarOffers.length <= 1
+ ? 100
+ : 100 - (100 * overallIndex) / (similarOffers.length - 1);
+
+ similarOffers = similarOffers.filter(
+ (offer) => offer.id !== overallHighestOffer.id,
+ );
+
+ const noOfSimilarOffers = similarOffers.length;
+ const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
+ const topPercentileOffers =
+ noOfSimilarOffers > 2
+ ? similarOffers.slice(
+ similarOffers90PercentileIndex,
+ similarOffers90PercentileIndex + 2,
+ )
+ : similarOffers;
+
+ const analysis = await ctx.prisma.offersAnalysis.create({
+ data: {
+ companyAnalysis: {
+ create: companyAnalysis.map((analysisUnit) => {
+ return {
+ companyName: analysisUnit.companyName,
+ noOfSimilarOffers: analysisUnit.noOfSimilarOffers,
+ percentile: analysisUnit.percentile,
+ topSimilarOffers: {
+ connect: analysisUnit.topSimilarOffers.map((offer) => {
+ return { id: offer.id };
+ }),
},
+ };
+ }),
+ },
+ overallAnalysis: {
+ create: {
+ companyName: overallHighestOffer.company.name,
+ noOfSimilarOffers,
+ percentile: overallPercentile,
+ topSimilarOffers: {
+ connect: topPercentileOffers.map((offer) => {
+ return { id: offer.id };
+ }),
},
},
+ },
+ overallHighestOffer: {
+ connect: {
+ id: overallHighestOffer.id,
+ },
+ },
+ profile: {
+ connect: {
+ id: input.profileId,
+ },
+ },
+ },
+ include: {
+ companyAnalysis: {
include: {
- companyAnalysis: {
+ topSimilarOffers: {
include: {
- topSimilarOffers: {
+ company: true,
+ location: {
include: {
- company: true,
- location: {
- include: {
- state: {
- include: {
- country: true,
- },
- },
- },
- },
- offersFullTime: {
- include: {
- totalCompensation: true,
- },
- },
- offersIntern: {
- include: {
- monthlySalary: true,
- },
- },
- profile: {
+ state: {
include: {
- background: {
- include: {
- experiences: {
- include: {
- company: true,
- location: {
- include: {
- state: {
- include: {
- country: true,
- },
- },
- },
- },
- },
- },
- },
- },
+ country: true,
},
},
},
},
- },
- },
- overallAnalysis: {
- include: {
- topSimilarOffers: {
+ offersFullTime: {
include: {
- company: true,
- location: {
- include: {
- state: {
- include: {
- country: true,
- },
- },
- },
- },
- offersFullTime: {
- include: {
- totalCompensation: true,
- },
- },
- offersIntern: {
- include: {
- monthlySalary: true,
- },
- },
- profile: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: {
include: {
- background: {
+ experiences: {
include: {
- experiences: {
+ company: true,
+ location: {
include: {
- company: true,
- location: {
+ state: {
include: {
- state: {
- include: {
- country: true,
- },
- },
+ country: true,
},
},
},
@@ -464,7 +412,11 @@ export const generateAnalysis = async (params: {
},
},
},
- overallHighestOffer: {
+ },
+ },
+ overallAnalysis: {
+ include: {
+ topSimilarOffers: {
include: {
company: true,
location: {
@@ -488,13 +440,61 @@ export const generateAnalysis = async (params: {
},
profile: {
include: {
- background: true,
+ background: {
+ include: {
+ experiences: {
+ include: {
+ company: true,
+ location: {
+ include: {
+ state: {
+ include: {
+ country: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
},
},
},
},
},
- });
+ },
+ overallHighestOffer: {
+ include: {
+ company: true,
+ location: {
+ include: {
+ state: {
+ include: {
+ country: true,
+ },
+ },
+ },
+ },
+ offersFullTime: {
+ include: {
+ totalCompensation: true,
+ },
+ },
+ offersIntern: {
+ include: {
+ monthlySalary: true,
+ },
+ },
+ profile: {
+ include: {
+ background: true,
+ },
+ },
+ },
+ },
+ },
+ });
- return profileAnalysisDtoMapper(analysis);
+ return profileAnalysisDtoMapper(analysis);
};
diff --git a/apps/portal/src/utils/offers/time.tsx b/apps/portal/src/utils/offers/time.tsx
index ffa3c545..5c7305dd 100644
--- a/apps/portal/src/utils/offers/time.tsx
+++ b/apps/portal/src/utils/offers/time.tsx
@@ -9,19 +9,23 @@ export function timeSinceNow(date: Date | number | string) {
let interval = seconds / 31536000;
if (interval > 1) {
- return `${Math.floor(interval)} years`;
+ const time: number = Math.floor(interval);
+ return time === 1 ? `${time} year` : `${time} years`;
}
interval = seconds / 2592000;
if (interval > 1) {
- return `${Math.floor(interval)} months`;
+ const time: number = Math.floor(interval);
+ return time === 1 ? `${time} month` : `${time} months`;
}
interval = seconds / 86400;
if (interval > 1) {
- return `${Math.floor(interval)} days`;
+ const time: number = Math.floor(interval);
+ return time === 1 ? `${time} day` : `${time} days`;
}
interval = seconds / 3600;
if (interval > 1) {
- return `${Math.floor(interval)} hours`;
+ const time: number = Math.floor(interval);
+ return time === 1 ? `${time} hour` : `${time} hours`;
}
interval = seconds / 60;
if (interval > 1) {
diff --git a/apps/portal/src/utils/questions/companySlug.ts b/apps/portal/src/utils/questions/companySlug.ts
new file mode 100644
index 00000000..bba52c4a
--- /dev/null
+++ b/apps/portal/src/utils/questions/companySlug.ts
@@ -0,0 +1,18 @@
+import type {
+ FilterChoice,
+ FilterOption,
+} from '~/components/questions/filter/FilterSection';
+
+export function companyOptionToSlug(option: FilterChoice): string {
+ return `${option.id}_${option.label}`;
+}
+
+export function slugToCompanyOption(slug: string): FilterOption {
+ const [id, label] = slug.split('_');
+ return {
+ checked: true,
+ id,
+ label,
+ value: id,
+ };
+}
diff --git a/apps/portal/src/utils/questions/locationSlug.ts b/apps/portal/src/utils/questions/locationSlug.ts
new file mode 100644
index 00000000..ad450ce0
--- /dev/null
+++ b/apps/portal/src/utils/questions/locationSlug.ts
@@ -0,0 +1,16 @@
+import type { TypeaheadOption } from '@tih/ui';
+
+import type { Location } from '~/types/questions';
+
+export function locationOptionToSlug(
+ value: Location & TypeaheadOption,
+): string {
+ return [
+ value.countryId,
+ value.stateId,
+ value.cityId,
+ value.id,
+ value.label,
+ value.value,
+ ].join('-');
+}
diff --git a/packages/ui/src/Banner/Banner.tsx b/packages/ui/src/Banner/Banner.tsx
index 1d41397d..24a0b873 100644
--- a/packages/ui/src/Banner/Banner.tsx
+++ b/packages/ui/src/Banner/Banner.tsx
@@ -19,7 +19,8 @@ export default function Banner({ children, size = 'md', onHide }: Props) {
size === 'xs' && 'text-xs',
)}>
-
+
{onHide != null && (
diff --git a/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx b/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx
index 6e2396dc..c9f3d2b1 100644
--- a/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx
+++ b/packages/ui/src/HorizontalDivider/HorizontalDivider.tsx
@@ -8,7 +8,7 @@ export default function HorizontalDivider({ className }: Props) {
return (
);
}
diff --git a/packages/ui/src/Tabs/Tabs.tsx b/packages/ui/src/Tabs/Tabs.tsx
index ac34722e..d387424b 100644
--- a/packages/ui/src/Tabs/Tabs.tsx
+++ b/packages/ui/src/Tabs/Tabs.tsx
@@ -28,7 +28,7 @@ export default function Tabs
({ label, tabs, value, onChange }: Props) {
children: tab.label,
className: clsx(
isSelected
- ? 'bg-indigo-100 text-indigo-700'
+ ? 'bg-primary-100 text-primary-700'
: 'hover:bg-slate-100 text-slate-500 hover:text-slate-700',
'px-3 py-2 font-medium text-sm rounded-md',
),
diff --git a/packages/ui/src/Typeahead/Typeahead.tsx b/packages/ui/src/Typeahead/Typeahead.tsx
index bce6fb32..8baf0776 100644
--- a/packages/ui/src/Typeahead/Typeahead.tsx
+++ b/packages/ui/src/Typeahead/Typeahead.tsx
@@ -29,17 +29,25 @@ type Props = Readonly<{
isLabelHidden?: boolean;
label: string;
noResultsMessage?: string;
- nullable?: boolean;
onQueryChange: (
value: string,
event: React.ChangeEvent,
) => void;
- onSelect: (option: TypeaheadOption | null) => void;
options: ReadonlyArray;
textSize?: TypeaheadTextSize;
value?: TypeaheadOption | null;
}> &
- Readonly;
+ Readonly &
+ (
+ | {
+ nullable: true;
+ onSelect: (option: TypeaheadOption | null) => void;
+ }
+ | {
+ nullable?: false;
+ onSelect: (option: TypeaheadOption) => void;
+ }
+ );
type State = 'error' | 'normal';