Merge branch 'main' into hongpo/update-question-filter

pull/409/head
hpkoh 3 years ago
commit 4bc794596b

@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `baseValue` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty.
- Added the required column `updatedAt` to the `OffersCurrency` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "OffersCurrency" ADD COLUMN "baseCurrency" TEXT NOT NULL DEFAULT 'USD',
ADD COLUMN "baseValue" INTEGER NOT NULL,
ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL;

@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "OffersCurrency" ALTER COLUMN "value" SET DATA TYPE DOUBLE PRECISION,
ALTER COLUMN "baseValue" SET DATA TYPE DOUBLE PRECISION;

@ -206,9 +206,9 @@ model OffersBackground {
totalYoe Int totalYoe Int
specificYoes OffersSpecificYoe[] specificYoes OffersSpecificYoe[]
experiences OffersExperience[] // For extensibility in the future experiences OffersExperience[]
educations OffersEducation[] // For extensibility in the future educations OffersEducation[]
profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade) profile OffersProfile @relation(fields: [offersProfileId], references: [id], onDelete: Cascade)
offersProfileId String @unique offersProfileId String @unique
@ -253,9 +253,15 @@ model OffersExperience {
model OffersCurrency { model OffersCurrency {
id String @id @default(cuid()) id String @id @default(cuid())
value Int createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
value Float
currency String currency String
baseValue Float
baseCurrency String @default("USD")
// Experience // Experience
OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation") OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation")
OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary") OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary")

@ -9,6 +9,7 @@ import {
YOE_CATEGORY, YOE_CATEGORY,
} from '~/components/offers/table/types'; } from '~/components/offers/table/types';
import { Currency } from '~/utils/offers/currency/CurrencyEnum';
import CurrencySelector from '~/utils/offers/currency/CurrencySelector'; import CurrencySelector from '~/utils/offers/currency/CurrencySelector';
import { trpc } from '~/utils/trpc'; import { trpc } from '~/utils/trpc';
@ -25,7 +26,7 @@ export default function OffersTable({
companyFilter, companyFilter,
jobTitleFilter, jobTitleFilter,
}: OffersTableProps) { }: OffersTableProps) {
const [currency, setCurrency] = useState('SGD'); // TODO: Detect location const [currency, setCurrency] = useState(Currency.SGD.toString()); // TODO: Detect location
const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY); const [selectedTab, setSelectedTab] = useState(YOE_CATEGORY.ENTRY);
const [pagination, setPagination] = useState<Paging>({ const [pagination, setPagination] = useState<Paging>({
currentPage: 0, currentPage: 0,
@ -44,12 +45,13 @@ export default function OffersTable({
numOfPages: 0, numOfPages: 0,
totalItems: 0, totalItems: 0,
}); });
}, [selectedTab]); }, [selectedTab, currency]);
const offersQuery = trpc.useQuery( const offersQuery = trpc.useQuery(
[ [
'offers.list', 'offers.list',
{ {
companyId: companyFilter, companyId: companyFilter,
currency,
limit: NUMBER_OF_OFFERS_IN_PAGE, limit: NUMBER_OF_OFFERS_IN_PAGE,
location: 'Singapore, Singapore', // TODO: Geolocation location: 'Singapore, Singapore', // TODO: Geolocation
offset: pagination.currentPage, offset: pagination.currentPage,

@ -42,7 +42,7 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
<div className="flex gap-2 pr-4"> <div className="flex gap-2 pr-4">
<ChatBubbleLeftIcon className="w-4" /> <ChatBubbleLeftIcon className="w-4" />
{`${resumeInfo.numComments} comment${ {`${resumeInfo.numComments} comment${
resumeInfo.numComments > 0 ? 's' : '' resumeInfo.numComments === 1 ? '' : 's'
}`} }`}
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
@ -51,7 +51,9 @@ export default function ResumeListItem({ href, resumeInfo }: Props) {
) : ( ) : (
<StarIcon className="w-4" /> <StarIcon className="w-4" />
)} )}
{resumeInfo.numStars} stars {`${resumeInfo.numStars} star${
resumeInfo.numStars === 1 ? '' : 's'
}`}
</div> </div>
</div> </div>
</div> </div>

@ -96,15 +96,17 @@ export default function ResumeCommentListItem({
<div className="flex flex-row space-x-1 pt-1 align-middle"> <div className="flex flex-row space-x-1 pt-1 align-middle">
<ResumeCommentVoteButtons commentId={comment.id} userId={userId} /> <ResumeCommentVoteButtons commentId={comment.id} userId={userId} />
{/* Action buttons; only present when not editing/replying */} {/* Action buttons; only present for authenticated user when not editing/replying */}
{isCommentOwner && !isEditingComment && !isReplyingComment && ( {userId && !isEditingComment && !isReplyingComment && (
<> <>
{isCommentOwner && (
<button <button
className="px-1 text-xs text-indigo-800 hover:text-indigo-400" className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
type="button" type="button"
onClick={() => setIsEditingComment(true)}> onClick={() => setIsEditingComment(true)}>
Edit Edit
</button> </button>
)}
{!comment.parentId && ( {!comment.parentId && (
<button <button

@ -9,7 +9,7 @@ type ContainerProps = {
export const Container: FC<ContainerProps> = ({ className, ...props }) => { export const Container: FC<ContainerProps> = ({ className, ...props }) => {
return ( return (
<div <div
className={clsx('mx-auto max-w-7xl px-4 sm:px-6 lg:px-8', className)} className={clsx('mx-auto max-w-7xl px-4 lg:px-2', className)}
{...props} {...props}
/> />
); );

@ -4,27 +4,27 @@ import { useEffect, useState } from 'react';
import { Tab } from '@headlessui/react'; import { Tab } from '@headlessui/react';
import { Container } from './Container'; import { Container } from './Container';
import screenshotExpenses from './images/screenshots/expenses.png'; import resumeBrowse from './images/screenshots/resumes-browse.png';
import screenshotPayroll from './images/screenshots/payroll.png'; import resumeReview from './images/screenshots/resumes-review.png';
import screenshotVatReturns from './images/screenshots/vat-returns.png'; import resumeSubmit from './images/screenshots/resumes-submit.png';
const features = [ const features = [
{ {
description: description:
'Browse the most popular reviewed resumes out there and see what you can learn', 'Browse the most popular reviewed resumes out there and see what you can learn',
image: screenshotPayroll, image: resumeBrowse,
title: 'Browse', title: 'Browse',
}, },
{ {
description: description:
'Upload your own resume easily to get feedback from people in industry.', 'Upload your own resume easily to get feedback from people in industry.',
image: screenshotExpenses, image: resumeSubmit,
title: 'Submit', title: 'Submit',
}, },
{ {
description: description:
'Pass the baton forward by reviewing resumes and bounce off ideas with other engineers out there.', 'Pass the baton forward by reviewing resumes and bounce off ideas with other engineers out there.',
image: screenshotVatReturns, image: resumeReview,
title: 'Review', title: 'Review',
}, },
]; ];
@ -49,7 +49,6 @@ export function PrimaryFeatures() {
return ( return (
<section <section
aria-label="Features for running your books"
className="relative overflow-hidden bg-gradient-to-r from-indigo-400 to-indigo-700 pt-20 pb-28 sm:py-32" className="relative overflow-hidden bg-gradient-to-r from-indigo-400 to-indigo-700 pt-20 pb-28 sm:py-32"
id="features"> id="features">
<Container className="relative"> <Container className="relative">
@ -64,7 +63,7 @@ export function PrimaryFeatures() {
vertical={tabOrientation === 'vertical'}> vertical={tabOrientation === 'vertical'}>
{({ selectedIndex }) => ( {({ selectedIndex }) => (
<> <>
<div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 sm:overflow-visible sm:pb-0 lg:col-span-5"> <div className="-mx-4 flex overflow-x-auto pb-4 sm:mx-0 sm:overflow-visible sm:pb-0 lg:col-span-4">
<Tab.List className="relative z-10 flex gap-x-4 whitespace-nowrap px-4 sm:mx-auto sm:px-0 lg:mx-0 lg:block lg:gap-x-0 lg:gap-y-1 lg:whitespace-normal"> <Tab.List className="relative z-10 flex gap-x-4 whitespace-nowrap px-4 sm:mx-auto sm:px-0 lg:mx-0 lg:block lg:gap-x-0 lg:gap-y-1 lg:whitespace-normal">
{features.map((feature, featureIndex) => ( {features.map((feature, featureIndex) => (
<div <div
@ -100,7 +99,7 @@ export function PrimaryFeatures() {
))} ))}
</Tab.List> </Tab.List>
</div> </div>
<Tab.Panels className="lg:col-span-7"> <Tab.Panels className="lg:col-span-8">
{features.map((feature) => ( {features.map((feature) => (
<Tab.Panel key={feature.title} unmount={false}> <Tab.Panel key={feature.title} unmount={false}>
<div className="relative sm:px-6 lg:hidden"> <div className="relative sm:px-6 lg:hidden">

@ -94,7 +94,6 @@ function QuoteIcon(props: QuoteProps) {
export function Testimonials() { export function Testimonials() {
return ( return (
<section <section
aria-label="What our customers are saying"
className="bg-gradient-to-r from-indigo-700 to-indigo-400 py-20 sm:py-32" className="bg-gradient-to-r from-indigo-700 to-indigo-400 py-20 sm:py-32"
id="testimonials"> id="testimonials">
<Container> <Container>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 652 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

@ -43,20 +43,29 @@ const analysisOfferDtoMapper = (
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & { totalCompensation: OffersCurrency })
| null; | null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null;
profile: OffersProfile & { background: OffersBackground | null }; profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<OffersExperience & { company: Company | null }>;
})
| null;
};
}, },
) => { ) => {
const { background, profileName } = offer.profile; const { background, profileName } = offer.profile;
const analysisOfferDto: AnalysisOffer = { const analysisOfferDto: AnalysisOffer = {
company: offersCompanyDtoMapper(offer.company), company: offersCompanyDtoMapper(offer.company),
id: offer.id, id: offer.id,
income: { currency: '', value: -1 }, income: { baseCurrency: '', baseValue: -1, currency: '', value: -1 },
jobType: offer.jobType, jobType: offer.jobType,
level: offer.offersFullTime?.level ?? '', level: offer.offersFullTime?.level ?? '',
location: offer.location, location: offer.location,
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
negotiationStrategy: offer.negotiationStrategy, negotiationStrategy: offer.negotiationStrategy,
previousCompanies: [], previousCompanies:
background?.experiences
?.filter((exp) => exp.company != null)
.map((exp) => exp.company?.name ?? '') ?? [],
profileName, profileName,
specialization: specialization:
offer.jobType === JobType.FULLTIME offer.jobType === JobType.FULLTIME
@ -74,10 +83,18 @@ const analysisOfferDtoMapper = (
offer.offersFullTime.totalCompensation.value; offer.offersFullTime.totalCompensation.value;
analysisOfferDto.income.currency = analysisOfferDto.income.currency =
offer.offersFullTime.totalCompensation.currency; offer.offersFullTime.totalCompensation.currency;
analysisOfferDto.income.baseValue =
offer.offersFullTime.totalCompensation.baseValue;
analysisOfferDto.income.baseCurrency =
offer.offersFullTime.totalCompensation.baseCurrency;
} else if (offer.offersIntern?.monthlySalary) { } else if (offer.offersIntern?.monthlySalary) {
analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value; analysisOfferDto.income.value = offer.offersIntern.monthlySalary.value;
analysisOfferDto.income.currency = analysisOfferDto.income.currency =
offer.offersIntern.monthlySalary.currency; offer.offersIntern.monthlySalary.currency;
analysisOfferDto.income.baseValue =
offer.offersIntern.monthlySalary.baseValue;
analysisOfferDto.income.baseCurrency =
offer.offersIntern.monthlySalary.baseCurrency;
} else { } else {
throw new TRPCError({ throw new TRPCError({
code: 'NOT_FOUND', code: 'NOT_FOUND',
@ -95,10 +112,26 @@ const analysisDtoMapper = (
OffersOffer & { OffersOffer & {
company: Company; company: Company;
offersFullTime: offersFullTime:
| (OffersFullTime & { totalCompensation: OffersCurrency }) | (OffersFullTime & {
totalCompensation: OffersCurrency;
})
| null; | null;
offersIntern: (OffersIntern & { monthlySalary: OffersCurrency }) | null; offersIntern:
profile: OffersProfile & { background: OffersBackground | null }; | (OffersIntern & {
monthlySalary: OffersCurrency;
})
| null;
profile: OffersProfile & {
background:
| (OffersBackground & {
experiences: Array<
OffersExperience & {
company: Company | null;
}
>;
})
| null;
};
} }
>, >,
) => { ) => {
@ -219,11 +252,15 @@ export const profileAnalysisDtoMapper = (
}; };
export const valuationDtoMapper = (currency: { export const valuationDtoMapper = (currency: {
baseCurrency: string;
baseValue: number;
currency: string; currency: string;
id?: string; id?: string;
value: number; value: number;
}) => { }) => {
const valuationDto: Valuation = { const valuationDto: Valuation = {
baseCurrency: currency.baseCurrency,
baseValue: currency.baseValue,
currency: currency.currency, currency: currency.currency,
value: currency.value, value: currency.value,
}; };
@ -554,7 +591,12 @@ export const dashboardOfferDtoMapper = (
const dashboardOfferDto: DashboardOffer = { const dashboardOfferDto: DashboardOffer = {
company: offersCompanyDtoMapper(offer.company), company: offersCompanyDtoMapper(offer.company),
id: offer.id, id: offer.id,
income: valuationDtoMapper({ currency: '', value: -1 }), income: valuationDtoMapper({
baseCurrency: '',
baseValue: -1,
currency: '',
value: -1,
}),
monthYearReceived: offer.monthYearReceived, monthYearReceived: offer.monthYearReceived,
profileId: offer.profileId, profileId: offer.profileId,
title: offer.offersFullTime?.title ?? '', title: offer.offersFullTime?.title ?? '',

@ -3,6 +3,7 @@ import type { Session } from 'next-auth';
import { SessionProvider } from 'next-auth/react'; import { SessionProvider } from 'next-auth/react';
import React from 'react'; import React from 'react';
import superjson from 'superjson'; import superjson from 'superjson';
import { ToastsProvider } from '@tih/ui';
import { httpBatchLink } from '@trpc/client/links/httpBatchLink'; import { httpBatchLink } from '@trpc/client/links/httpBatchLink';
import { loggerLink } from '@trpc/client/links/loggerLink'; import { loggerLink } from '@trpc/client/links/loggerLink';
import { withTRPC } from '@trpc/next'; import { withTRPC } from '@trpc/next';
@ -19,9 +20,11 @@ const MyApp: AppType<{ session: Session | null }> = ({
}) => { }) => {
return ( return (
<SessionProvider session={session}> <SessionProvider session={session}>
<ToastsProvider>
<AppShell> <AppShell>
<Component {...pageProps} /> <Component {...pageProps} />
</AppShell> </AppShell>
</ToastsProvider>
</SessionProvider> </SessionProvider>
); );
}; };

@ -1,32 +1,11 @@
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { HorizontalDivider } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import type { Month, MonthYear } from '~/components/shared/MonthYearPicker';
import MonthYearPicker from '~/components/shared/MonthYearPicker';
export default function HomePage() { export default function HomePage() {
const [selectedCompany, setSelectedCompany] =
useState<TypeaheadOption | null>(null);
const [monthYear, setMonthYear] = useState<MonthYear>({
month: (new Date().getMonth() + 1) as Month,
year: new Date().getFullYear(),
});
return ( return (
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">
<div className="flex h-full items-center justify-center"> <div className="flex h-full items-center justify-center">
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-primary-600 text-center text-4xl font-bold"> <h1 className="text-primary-600 text-center text-4xl font-bold">
Homepage Tech Interview Handbook Portal
</h1> </h1>
<CompaniesTypeahead
onSelect={(option) => setSelectedCompany(option)}
/>
<pre>{JSON.stringify(selectedCompany, null, 2)}</pre>
<HorizontalDivider />
<MonthYearPicker value={monthYear} onChange={setMonthYear} />
</div> </div>
</div> </div>
</main> </main>

@ -40,7 +40,7 @@ function Test() {
deleteCommentMutation.mutate({ deleteCommentMutation.mutate({
id: 'cl97fprun001j7iyg6ev9x983', id: 'cl97fprun001j7iyg6ev9x983',
profileId: 'cl96stky5002ew32gx2kale2x', profileId: 'cl96stky5002ew32gx2kale2x',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1', token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
userId: 'cl97dl51k001e7iygd5v5gt58', userId: 'cl97dl51k001e7iygd5v5gt58',
}); });
}; };
@ -84,7 +84,7 @@ function Test() {
const handleLink = () => { const handleLink = () => {
addToUserProfileMutation.mutate({ addToUserProfileMutation.mutate({
profileId: 'cl9efyn9p004ww3u42mjgl1vn', profileId: 'cl9efyn9p004ww3u42mjgl1vn',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba', token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
userId: 'cl9ehvpng0000w3ec2mpx0bdd', userId: 'cl9ehvpng0000w3ec2mpx0bdd',
}); });
}; };
@ -103,11 +103,10 @@ function Test() {
], ],
experiences: [ experiences: [
{ {
companyId: 'cl9h0bqu50000txxwkhmshhxz', companyId: 'cl9j4yawz0003utlp1uaa1t8o',
durationInMonths: 24, durationInMonths: 24,
jobType: 'FULLTIME', jobType: 'FULLTIME',
level: 'Junior', level: 'Junior',
// "monthlySalary": undefined,
specialization: 'Front End', specialization: 'Front End',
title: 'Software Engineer', title: 'Software Engineer',
totalCompensation: { totalCompensation: {
@ -132,7 +131,7 @@ function Test() {
{ {
comments: 'I am a Raffles Institution almumni', comments: 'I am a Raffles Institution almumni',
// Comments: '', // Comments: '',
companyId: 'cl9h0bqu50000txxwkhmshhxz', companyId: 'cl9j4yawz0003utlp1uaa1t8o',
jobType: 'FULLTIME', jobType: 'FULLTIME',
location: 'Singapore, Singapore', location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
@ -140,28 +139,28 @@ function Test() {
offersFullTime: { offersFullTime: {
baseSalary: { baseSalary: {
currency: 'SGD', currency: 'SGD',
value: 84000, value: 2222,
}, },
bonus: { bonus: {
currency: 'SGD', currency: 'SGD',
value: 20000, value: 2222,
}, },
level: 'Junior', level: 'Junior',
specialization: 'Front End', specialization: 'Front End',
stocks: { stocks: {
currency: 'SGD', currency: 'SGD',
value: 100, value: 0,
}, },
title: 'Software Engineer', title: 'Software Engineer',
totalCompensation: { totalCompensation: {
currency: 'SGD', currency: 'SGD',
value: 104100, value: 4444,
}, },
}, },
}, },
{ {
comments: '', comments: '',
companyId: 'cl9h0bqu50000txxwkhmshhxz', companyId: 'cl9j4yawz0003utlp1uaa1t8o',
jobType: 'FULLTIME', jobType: 'FULLTIME',
location: 'Singapore, Singapore', location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'), monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
@ -192,14 +191,14 @@ function Test() {
}); });
}; };
const profileId = 'cl9i68fv60000tthj8t3zkox0'; // Remember to change this filed after testing deleting const profileId = 'cl9j50xzk008vutfqg6mta2ey'; // Remember to change this filed after testing deleting
const data = trpc.useQuery( const data = trpc.useQuery(
[ [
`offers.profile.listOne`, `offers.profile.listOne`,
{ {
profileId, profileId,
token: token:
'd14666ff76e267c9e99445844b41410e83874936d0c07e664db73ff0ea76919e', '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
}, },
], ],
{ {
@ -223,7 +222,7 @@ function Test() {
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
deleteMutation.mutate({ deleteMutation.mutate({
profileId: id, profileId: id,
token: 'e7effd2a40adba2deb1ddea4fb9f1e6c3c98ab0a85a88ed1567fc2a107fdb445', token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
}); });
}; };
@ -257,15 +256,15 @@ function Test() {
createdAt: new Date('2022-10-12T16:19:05.196Z'), createdAt: new Date('2022-10-12T16:19:05.196Z'),
description: 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.', '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: 'cl9h0bqug0003txxwgkac0x40', id: 'cl9j4yawz0003utlp1uaa1t8o',
logoUrl: 'https://logo.clearbit.com/meta.com', logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta', name: 'Meta',
slug: 'meta', slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'), updatedAt: new Date('2022-10-12T16:19:05.196Z'),
}, },
companyId: 'cl9h0bqug0003txxwgkac0x40', companyId: 'cl9j4yawz0003utlp1uaa1t8o',
durationInMonths: 24, durationInMonths: 24,
// Id: 'cl9h0bqug0003txxwgkac0x40', // Id: 'cl9j4yawz0003utlp1uaa1t8o',
jobType: 'FULLTIME', jobType: 'FULLTIME',
level: 'Junior', level: 'Junior',
monthlySalary: null, monthlySalary: null,
@ -309,13 +308,13 @@ function Test() {
createdAt: new Date('2022-10-12T16:19:05.196Z'), createdAt: new Date('2022-10-12T16:19:05.196Z'),
description: 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.', '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: 'cl9h0bqug0003txxwgkac0x40', id: 'cl9j4yawz0003utlp1uaa1t8o',
logoUrl: 'https://logo.clearbit.com/meta.com', logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta', name: 'Meta',
slug: 'meta', slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'), updatedAt: new Date('2022-10-12T16:19:05.196Z'),
}, },
companyId: 'cl9h0bqug0003txxwgkac0x40', companyId: 'cl9j4yawz0003utlp1uaa1t8o',
id: 'cl9i68fve000ntthj5h9yvqnh', id: 'cl9i68fve000ntthj5h9yvqnh',
jobType: 'FULLTIME', jobType: 'FULLTIME',
location: 'Singapore, Singapore', location: 'Singapore, Singapore',
@ -362,13 +361,13 @@ function Test() {
// createdAt: new Date('2022-10-12T16:19:05.196Z'), // createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description: // 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.', // '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: 'cl9h0bqug0003txxwgkac0x40', // id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com', // logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta', // name: 'Meta',
// slug: 'meta', // slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'), // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// }, // },
// companyId: 'cl9h0bqug0003txxwgkac0x40', // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl9i68fvf000ytthj0ltsqt1d', // id: 'cl9i68fvf000ytthj0ltsqt1d',
// jobType: 'FULLTIME', // jobType: 'FULLTIME',
// location: 'Singapore, Singapore', // location: 'Singapore, Singapore',
@ -415,13 +414,13 @@ function Test() {
// createdAt: new Date('2022-10-12T16:19:05.196Z'), // createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description: // 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.', // '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: 'cl9h0bqug0003txxwgkac0x40', // id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com', // logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta', // name: 'Meta',
// slug: 'meta', // slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'), // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// }, // },
// companyId: 'cl9h0bqug0003txxwgkac0x40', // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl96stky9003bw32gc3l955vr', // id: 'cl96stky9003bw32gc3l955vr',
// jobType: 'FULLTIME', // jobType: 'FULLTIME',
// location: 'Singapore, Singapore', // location: 'Singapore, Singapore',
@ -468,13 +467,13 @@ function Test() {
// createdAt: new Date('2022-10-12T16:19:05.196Z'), // createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description: // 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.', // '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: 'cl9h0bqug0003txxwgkac0x40', // id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com', // logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta', // name: 'Meta',
// slug: 'meta', // slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'), // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// }, // },
// companyId: 'cl9h0bqug0003txxwgkac0x40', // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl976wf28000t7iyga4noyz7s', // id: 'cl976wf28000t7iyga4noyz7s',
// jobType: 'FULLTIME', // jobType: 'FULLTIME',
// location: 'Singapore, Singapore', // location: 'Singapore, Singapore',
@ -521,13 +520,13 @@ function Test() {
// createdAt: new Date('2022-10-12T16:19:05.196Z'), // createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description: // 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.', // '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: 'cl9h0bqug0003txxwgkac0x40', // id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com', // logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta', // name: 'Meta',
// slug: 'meta', // slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'), // updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// }, // },
// companyId: 'cl9h0bqug0003txxwgkac0x40', // companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl96tbb3o0051w32gjrpaiiit', // id: 'cl96tbb3o0051w32gjrpaiiit',
// jobType: 'FULLTIME', // jobType: 'FULLTIME',
// location: 'Singapore, Singapore', // location: 'Singapore, Singapore',
@ -570,7 +569,7 @@ function Test() {
// }, // },
], ],
// ProfileName: 'ailing bryann stuart ziqing', // ProfileName: 'ailing bryann stuart ziqing',
token: 'd3509cb890f0bae0a785afdd6c1c074a140706ab1d155ed338ec22dcca5c92f1', token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
userId: null, userId: null,
}); });
}; };

@ -8,7 +8,7 @@ function GenerateAnalysis() {
return ( return (
<div> <div>
{JSON.stringify( {JSON.stringify(
analysisMutation.mutate({ profileId: 'cl9h23fb1002ftxysli5iziu2' }), analysisMutation.mutate({ profileId: 'cl9jj2ks1001li9fn9np47wjr' }),
)} )}
</div> </div>
); );

@ -5,7 +5,7 @@ import { trpc } from '~/utils/trpc';
function GetAnalysis() { function GetAnalysis() {
const analysis = trpc.useQuery([ const analysis = trpc.useQuery([
'offers.analysis.get', 'offers.analysis.get',
{ profileId: 'cl9h23fb1002ftxysli5iziu2' }, { profileId: 'cl9jj2ks1001li9fn9np47wjr' },
]); ]);
return <div>{JSON.stringify(analysis.data)}</div>; return <div>{JSON.stringify(analysis.data)}</div>;

@ -6,11 +6,12 @@ function Test() {
const data = trpc.useQuery([ const data = trpc.useQuery([
'offers.list', 'offers.list',
{ {
currency: 'SGD',
limit: 100, limit: 100,
location: 'Singapore, Singapore', location: 'Singapore, Singapore',
offset: 0, offset: 0,
sortBy: '+totalCompensation', sortBy: '-totalCompensation',
yoeCategory: 1, yoeCategory: 2,
}, },
]); ]);

@ -131,7 +131,9 @@ export default function ResumeReviewPage() {
onClick={onStarButtonClick}> onClick={onStarButtonClick}>
<span className="relative inline-flex"> <span className="relative inline-flex">
<div className="-ml-1 mr-2 h-5 w-5"> <div className="-ml-1 mr-2 h-5 w-5">
{starMutation.isLoading || unstarMutation.isLoading ? ( {starMutation.isLoading ||
unstarMutation.isLoading ||
detailsQuery.isLoading ? (
<Spinner className="mt-0.5" size="xs" /> <Spinner className="mt-0.5" size="xs" />
) : ( ) : (
<StarIcon <StarIcon

@ -1,9 +1,10 @@
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { Disclosure } from '@headlessui/react'; import { Dialog, Disclosure, Transition } from '@headlessui/react';
import { MinusIcon, PlusIcon } from '@heroicons/react/20/solid'; import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { import {
MagnifyingGlassIcon, MagnifyingGlassIcon,
NewspaperIcon, NewspaperIcon,
@ -13,6 +14,7 @@ import {
CheckboxList, CheckboxList,
DropdownMenu, DropdownMenu,
Pagination, Pagination,
Spinner,
Tabs, Tabs,
TextInput, TextInput,
} from '@tih/ui'; } from '@tih/ui';
@ -104,6 +106,7 @@ export default function ResumeHomePage() {
const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE); const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE);
const [shortcutSelected, setShortcutSelected] = useState('All'); const [shortcutSelected, setShortcutSelected] = useState('All');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const skip = (currentPage - 1) * PAGE_LIMIT; const skip = (currentPage - 1) * PAGE_LIMIT;
@ -240,86 +243,137 @@ export default function ResumeHomePage() {
<Head> <Head>
<title>Resume Review Portal</title> <title>Resume Review Portal</title>
</Head> </Head>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-scroll">
<div className="ml-4 py-4"> {/* Mobile Filters */}
<ResumeReviewsTitle />
</div>
<div className="mt-4 flex items-start">
<div className="w-screen sm:px-4 md:px-8">
<div className="grid grid-cols-12">
<div className="col-span-2 self-end">
<h3 className="text-md mb-4 font-medium tracking-tight text-gray-900">
Shortcuts:
</h3>
</div>
<div className="col-span-10">
<div className="border-grey-200 grid grid-cols-12 items-center gap-4 border-b pb-2">
<div className="col-span-5">
<Tabs
label="Resume Browse Tabs"
tabs={[
{
label: 'All Resumes',
value: BROWSE_TABS_VALUES.ALL,
},
{
label: 'Starred Resumes',
value: BROWSE_TABS_VALUES.STARRED,
},
{
label: 'My Resumes',
value: BROWSE_TABS_VALUES.MY,
},
]}
value={tabsValue}
onChange={onTabChange}
/>
</div>
<div className="col-span-7 flex items-center justify-evenly">
<div className="w-64">
<form>
<TextInput
label=""
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
type="text"
value={searchValue}
onChange={setSearchValue}
/>
</form>
</div>
<div>
<DropdownMenu align="end" label={SORT_OPTIONS[sortOrder]}>
{Object.entries(SORT_OPTIONS).map(([key, value]) => (
<DropdownMenu.Item
key={key}
isSelected={sortOrder === key}
label={value}
onClick={() =>
setSortOrder(key)
}></DropdownMenu.Item>
))}
</DropdownMenu>
</div>
<div> <div>
<Transition.Root as={Fragment} show={mobileFiltersOpen}>
<Dialog
as="div"
className="relative z-40 lg:hidden"
onClose={setMobileFiltersOpen}>
<Transition.Child
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0">
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 z-40 flex">
<Transition.Child
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="translate-x-full">
<Dialog.Panel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-auto bg-white py-4 pb-12 shadow-xl">
<div className="flex items-center justify-between px-4">
<h2 className="text-lg font-medium text-gray-900">
Shortcuts
</h2>
<button <button
className="rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white" className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-gray-400"
type="button" type="button"
onClick={onSubmitResume}> onClick={() => setMobileFiltersOpen(false)}>
Submit Resume <span className="sr-only">Close menu</span>
<XMarkIcon aria-hidden="true" className="h-6 w-6" />
</button> </button>
</div> </div>
<form className="mt-4 border-t border-gray-200">
<ul
className="flex flex-wrap justify-start gap-4 px-4 py-3 font-medium text-gray-900"
role="list">
{SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}>
<ResumeFilterPill
isSelected={shortcutSelected === shortcut.name}
title={shortcut.name}
onClick={() => onShortcutChange(shortcut)}
/>
</li>
))}
</ul>
{filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-t border-gray-200 px-4 py-6">
{({ open }) => (
<>
<h3 className="-mx-2 -my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between bg-white px-2 py-3 text-gray-400 hover:text-gray-500">
<span className="font-medium text-gray-900">
{filter.label}
</span>
<span className="ml-6 flex items-center">
{open ? (
<MinusIcon
aria-hidden="true"
className="h-5 w-5"
/>
) : (
<PlusIcon
aria-hidden="true"
className="h-5 w-5"
/>
)}
</span>
</Disclosure.Button>
</h3>
<Disclosure.Panel className="pt-6">
<div className="space-y-6">
{filter.options.map((option) => (
<div
key={option.value}
className="[&>div>div:nth-child(2)>label]:font-normal [&>div>div:nth-child(1)>input]:text-indigo-600 [&>div>div:nth-child(1)>input]:ring-indigo-500">
<CheckboxInput
label={option.label}
value={userFilters[filter.id].includes(
option.value,
)}
onChange={(isChecked) =>
onFilterCheckboxChange(
isChecked,
filter.id,
option.value,
)
}
/>
</div>
))}
</div> </div>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</Dialog.Panel>
</Transition.Child>
</div> </div>
</Dialog>
</Transition.Root>
</div> </div>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-auto">
<div className="ml-4 py-4">
<ResumeReviewsTitle />
</div> </div>
<div className="grid grid-cols-12"> <div className="mx-8 mt-4 flex justify-start">
<div className="col-span-2"> <div className="hidden w-1/6 pt-2 lg:block">
<h3 className="text-md mb-4 font-medium tracking-tight text-gray-900">
Shortcuts:
</h3>
<div className="w-100 pt-4 sm:pr-0 md:pr-4"> <div className="w-100 pt-4 sm:pr-0 md:pr-4">
<form> <form>
<h3 className="sr-only">Shortcuts</h3>
<ul <ul
className="flex flex-wrap justify-start gap-4 pb-6 text-sm font-medium text-gray-900" className="flex flex-wrap justify-start gap-4 pb-6 text-sm font-medium text-gray-900"
role="list"> role="list">
@ -397,8 +451,92 @@ export default function ResumeHomePage() {
</form> </form>
</div> </div>
</div> </div>
<div className="col-span-10 mb-6"> <div className="w-full">
{sessionData === null && <div className="lg:border-grey-200 flex flex-wrap items-center justify-between pb-2 lg:border-b">
<div className="border-grey-200 mb-4 flex w-full justify-between border-b pb-2 lg:mb-0 lg:w-auto lg:border-none lg:pb-0">
<div>
<Tabs
label="Resume Browse Tabs"
tabs={[
{
label: 'All Resumes',
value: BROWSE_TABS_VALUES.ALL,
},
{
label: 'Starred Resumes',
value: BROWSE_TABS_VALUES.STARRED,
},
{
label: 'My Resumes',
value: BROWSE_TABS_VALUES.MY,
},
]}
value={tabsValue}
onChange={onTabChange}
/>
</div>
<div>
<button
className="ml-4 rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white lg:hidden"
type="button"
onClick={onSubmitResume}>
Submit Resume
</button>
</div>
</div>
<div className="flex flex-wrap items-center justify-start gap-8">
<div className="w-64">
<form>
<TextInput
label=""
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
type="text"
value={searchValue}
onChange={setSearchValue}
/>
</form>
</div>
<div>
<DropdownMenu align="end" label={SORT_OPTIONS[sortOrder]}>
{Object.entries(SORT_OPTIONS).map(([key, value]) => (
<DropdownMenu.Item
key={key}
isSelected={sortOrder === key}
label={value}
onClick={() => setSortOrder(key)}></DropdownMenu.Item>
))}
</DropdownMenu>
</div>
<button
className="-m-2 text-gray-400 hover:text-gray-500 lg:hidden"
type="button"
onClick={() => setMobileFiltersOpen(true)}>
<span className="sr-only">Filters</span>
<FunnelIcon aria-hidden="true" className="h-6 w-6" />
</button>
<div>
<button
className="hidden w-36 rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white lg:block"
type="button"
onClick={onSubmitResume}>
Submit Resume
</button>
</div>
</div>
</div>
<div className="mb-6">
{allResumesQuery.isLoading ||
starredResumesQuery.isLoading ||
myResumesQuery.isLoading ? (
<div className="w-full pt-4">
{' '}
<Spinner display="block" size="lg" />{' '}
</div>
) : sessionData === null &&
tabsValue !== BROWSE_TABS_VALUES.ALL ? ( tabsValue !== BROWSE_TABS_VALUES.ALL ? (
<ResumeSignInButton <ResumeSignInButton
className="mt-8" className="mt-8"
@ -437,7 +575,6 @@ export default function ResumeHomePage() {
</div> </div>
</div> </div>
</div> </div>
</div>
</main> </main>
</> </>
); );

@ -3,7 +3,6 @@ import Head from 'next/head';
import { CallToAction } from '~/components/resumes/landing/CallToAction'; import { CallToAction } from '~/components/resumes/landing/CallToAction';
import { Hero } from '~/components/resumes/landing/Hero'; import { Hero } from '~/components/resumes/landing/Hero';
import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures'; import { PrimaryFeatures } from '~/components/resumes/landing/PrimaryFeatures';
import { Testimonials } from '~/components/resumes/landing/Testimonials';
export default function Home() { export default function Home() {
return ( return (
@ -16,7 +15,6 @@ export default function Home() {
<Hero /> <Hero />
<PrimaryFeatures /> <PrimaryFeatures />
<CallToAction /> <CallToAction />
<Testimonials />
</main> </main>
</> </>
); );

@ -80,6 +80,7 @@ export default function SubmitResumeForm({
const { data: session, status } = useSession(); const { data: session, status } = useSession();
const router = useRouter(); const router = useRouter();
const trpcContext = trpc.useContext();
const resumeUpsertMutation = trpc.useMutation('resumes.resume.user.upsert'); const resumeUpsertMutation = trpc.useMutation('resumes.resume.user.upsert');
const isNewForm = initFormDetails == null; const isNewForm = initFormDetails == null;
@ -170,6 +171,7 @@ export default function SubmitResumeForm({
}, },
onSuccess() { onSuccess() {
if (isNewForm) { if (isNewForm) {
trpcContext.invalidateQueries('resumes.resume.findAll');
router.push('/resumes/browse'); router.push('/resumes/browse');
} else { } else {
onClose(); onClose();
@ -228,7 +230,7 @@ export default function SubmitResumeForm({
<Head> <Head>
<title>Upload a Resume</title> <title>Upload a Resume</title>
</Head> </Head>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-scroll"> <main className="h-[calc(100vh-4rem)] flex-1 overflow-y-auto">
<section <section
aria-labelledby="primary-heading" aria-labelledby="primary-heading"
className="flex h-full min-w-0 flex-1 flex-col lg:order-last"> className="flex h-full min-w-0 flex-1 flex-col lg:order-last">

@ -0,0 +1,51 @@
import { useState } from 'react';
import type { TypeaheadOption } from '@tih/ui';
import { Button } from '@tih/ui';
import { useToast } from '@tih/ui';
import { HorizontalDivider } from '@tih/ui';
import CompaniesTypeahead from '~/components/shared/CompaniesTypeahead';
import type { Month, MonthYear } from '~/components/shared/MonthYearPicker';
import MonthYearPicker from '~/components/shared/MonthYearPicker';
export default function HomePage() {
const [selectedCompany, setSelectedCompany] =
useState<TypeaheadOption | null>(null);
const [monthYear, setMonthYear] = useState<MonthYear>({
month: (new Date().getMonth() + 1) as Month,
year: new Date().getFullYear(),
});
const { showToast } = useToast();
return (
<main className="flex-1 overflow-y-auto">
<div className="flex h-full items-center justify-center">
<div className="space-y-4">
<h1 className="text-primary-600 text-center text-4xl font-bold">
Test Page
</h1>
<CompaniesTypeahead
onSelect={(option) => setSelectedCompany(option)}
/>
<pre>{JSON.stringify(selectedCompany, null, 2)}</pre>
<HorizontalDivider />
<MonthYearPicker value={monthYear} onChange={setMonthYear} />
<HorizontalDivider />
<Button
label="Add toast"
variant="primary"
onClick={() => {
showToast({
// Duration: 10000 (optional)
subtitle: `Some optional subtitle ${Date.now()}`,
title: `Hello World ${Date.now()}`,
variant: 'success',
});
}}
/>
</div>
</div>
</main>
);
}

@ -187,14 +187,14 @@ export const offersAnalysisRouter = createRouter()
{ {
offersFullTime: { offersFullTime: {
totalCompensation: { totalCompensation: {
value: 'desc', baseValue: 'desc',
}, },
}, },
}, },
{ {
offersIntern: { offersIntern: {
monthlySalary: { monthlySalary: {
value: 'desc', baseValue: 'desc',
}, },
}, },
}, },
@ -216,15 +216,17 @@ export const offersAnalysisRouter = createRouter()
// TODO: Shift yoe out of background to make it mandatory // TODO: Shift yoe out of background to make it mandatory
if ( if (
!overallHighestOffer.profile.background || !overallHighestOffer.profile.background ||
overallHighestOffer.profile.background.totalYoe === undefined overallHighestOffer.profile.background.totalYoe == null
) { ) {
throw new TRPCError({ throw new TRPCError({
code: 'BAD_REQUEST', code: 'NOT_FOUND',
message: 'Cannot analyse without YOE', message: 'YOE not found',
}); });
} }
const yoe = overallHighestOffer.profile.background.totalYoe as number; 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: { include: {
@ -257,14 +259,14 @@ export const offersAnalysisRouter = createRouter()
{ {
offersFullTime: { offersFullTime: {
totalCompensation: { totalCompensation: {
value: 'desc', baseValue: 'desc',
}, },
}, },
}, },
{ {
offersIntern: { offersIntern: {
monthlySalary: { monthlySalary: {
value: 'desc', baseValue: 'desc',
}, },
}, },
}, },
@ -274,17 +276,20 @@ export const offersAnalysisRouter = createRouter()
{ {
location: overallHighestOffer.location, location: overallHighestOffer.location,
}, },
{
monthYearReceived: {
gte: monthYearReceived,
},
},
{ {
OR: [ OR: [
{ {
offersFullTime: { offersFullTime: {
level: overallHighestOffer.offersFullTime?.level, level: overallHighestOffer.offersFullTime?.level,
specialization: title: overallHighestOffer.offersFullTime?.title,
overallHighestOffer.offersFullTime?.specialization,
}, },
offersIntern: { offersIntern: {
specialization: title: overallHighestOffer.offersIntern?.title,
overallHighestOffer.offersIntern?.specialization,
}, },
}, },
], ],
@ -317,7 +322,9 @@ export const offersAnalysisRouter = createRouter()
similarOffers, similarOffers,
); );
const overallPercentile = const overallPercentile =
similarOffers.length === 0 ? 0 : overallIndex / similarOffers.length; similarOffers.length === 0
? 100
: (100 * overallIndex) / similarOffers.length;
const companyIndex = searchOfferPercentile( const companyIndex = searchOfferPercentile(
overallHighestOffer, overallHighestOffer,
@ -325,10 +332,11 @@ export const offersAnalysisRouter = createRouter()
); );
const companyPercentile = const companyPercentile =
similarCompanyOffers.length === 0 similarCompanyOffers.length === 0
? 0 ? 100
: companyIndex / similarCompanyOffers.length; : (100 * companyIndex) / similarCompanyOffers.length;
// FIND TOP >=90 PERCENTILE OFFERS // FIND TOP >=90 PERCENTILE OFFERS, DOESN'T GIVE 100th PERCENTILE
// e.g. If there only 4 offers, it gives the 2nd and 3rd offer
similarOffers = similarOffers.filter( similarOffers = similarOffers.filter(
(offer) => offer.id !== overallHighestOffer.id, (offer) => offer.id !== overallHighestOffer.id,
); );
@ -337,10 +345,9 @@ export const offersAnalysisRouter = createRouter()
); );
const noOfSimilarOffers = similarOffers.length; const noOfSimilarOffers = similarOffers.length;
const similarOffers90PercentileIndex = const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
Math.floor(noOfSimilarOffers * 0.9) - 1;
const topPercentileOffers = const topPercentileOffers =
noOfSimilarOffers > 1 noOfSimilarOffers > 2
? similarOffers.slice( ? similarOffers.slice(
similarOffers90PercentileIndex, similarOffers90PercentileIndex,
similarOffers90PercentileIndex + 2, similarOffers90PercentileIndex + 2,
@ -348,10 +355,11 @@ export const offersAnalysisRouter = createRouter()
: similarOffers; : similarOffers;
const noOfSimilarCompanyOffers = similarCompanyOffers.length; const noOfSimilarCompanyOffers = similarCompanyOffers.length;
const similarCompanyOffers90PercentileIndex = const similarCompanyOffers90PercentileIndex = Math.ceil(
Math.floor(noOfSimilarCompanyOffers * 0.9) - 1; noOfSimilarCompanyOffers * 0.1,
);
const topPercentileCompanyOffers = const topPercentileCompanyOffers =
noOfSimilarCompanyOffers > 1 noOfSimilarCompanyOffers > 2
? similarCompanyOffers.slice( ? similarCompanyOffers.slice(
similarCompanyOffers90PercentileIndex, similarCompanyOffers90PercentileIndex,
similarCompanyOffers90PercentileIndex + 2, similarCompanyOffers90PercentileIndex + 2,

@ -26,26 +26,27 @@ export const offersCommentsRouter = createRouter()
user: true, user: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc' createdAt: 'desc',
} },
}, },
replyingTo: true, replyingTo: true,
user: true, user: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc' createdAt: 'desc',
} },
}, },
}, },
where: { where: {
id: input.profileId, id: input.profileId,
} },
}); });
const discussions: OffersDiscussion = { const discussions: OffersDiscussion = {
data: result?.discussion data:
result?.discussion
.filter((x) => { .filter((x) => {
return x.replyingToId === null return x.replyingToId === null;
}) })
.map((x) => { .map((x) => {
if (x.user == null) { if (x.user == null) {
@ -81,18 +82,18 @@ export const offersCommentsRouter = createRouter()
message: reply.message, message: reply.message,
replies: [], replies: [],
replyingToId: reply.replyingToId, replyingToId: reply.replyingToId,
user: reply.user user: reply.user,
} };
}), }),
replyingToId: x.replyingToId, replyingToId: x.replyingToId,
user: x.user user: x.user,
} };
return replyType return replyType;
}) ?? [] }) ?? [],
} };
return discussions return discussions;
}, },
}) })
.mutation('create', { .mutation('create', {
@ -101,7 +102,7 @@ export const offersCommentsRouter = createRouter()
profileId: z.string(), profileId: z.string(),
replyingToId: z.string().optional(), replyingToId: z.string().optional(),
token: z.string().optional(), token: z.string().optional(),
userId: z.string().optional() userId: z.string().optional(),
}), }),
async resolve({ ctx, input }) { async resolve({ ctx, input }) {
const profile = await ctx.prisma.offersProfile.findFirst({ const profile = await ctx.prisma.offersProfile.findFirst({
@ -156,7 +157,7 @@ export const offersCommentsRouter = createRouter()
const created = await ctx.prisma.offersReply.findFirst({ const created = await ctx.prisma.offersReply.findFirst({
include: { include: {
user: true user: true,
}, },
where: { where: {
id: createdReply.id, id: createdReply.id,
@ -175,10 +176,10 @@ export const offersCommentsRouter = createRouter()
id: '', id: '',
image: '', image: '',
name: profile?.profileName ?? '<missing name>', name: profile?.profileName ?? '<missing name>',
} },
} };
return result return result;
} }
throw new trpc.TRPCError({ throw new trpc.TRPCError({
@ -223,10 +224,10 @@ export const offersCommentsRouter = createRouter()
include: { include: {
replies: { replies: {
include: { include: {
user: true user: true,
} },
}, },
user: true user: true,
}, },
where: { where: {
id: input.id, id: input.id,
@ -250,8 +251,8 @@ export const offersCommentsRouter = createRouter()
id: '', id: '',
image: '', image: '',
name: profile?.profileName ?? '<missing name>', name: profile?.profileName ?? '<missing name>',
} },
} };
}), }),
replyingToId: updated!.replyingToId, replyingToId: updated!.replyingToId,
user: updated!.user ?? { user: updated!.user ?? {
@ -260,10 +261,10 @@ export const offersCommentsRouter = createRouter()
id: '', id: '',
image: '', image: '',
name: profile?.profileName ?? '<missing name>', name: profile?.profileName ?? '<missing name>',
} },
} };
return result return result;
} }
throw new trpc.TRPCError({ throw new trpc.TRPCError({

@ -1,5 +1,6 @@
import crypto, { randomUUID } from 'crypto'; import crypto, { randomUUID } from 'crypto';
import { z } from 'zod'; import { z } from 'zod';
import { JobType } from '@prisma/client';
import * as trpc from '@trpc/server'; import * as trpc from '@trpc/server';
import { import {
@ -7,6 +8,9 @@ import {
createOfferProfileResponseMapper, createOfferProfileResponseMapper,
profileDtoMapper, profileDtoMapper,
} from '~/mappers/offers-mappers'; } from '~/mappers/offers-mappers';
import { baseCurrencyString } from '~/utils/offers/currency';
import { convert } from '~/utils/offers/currency/currencyExchange';
import { createValidationRegex } from '~/utils/offers/zodRegex';
import { createRouter } from '../context'; import { createRouter } from '../context';
@ -31,7 +35,7 @@ const offer = z.object({
company: company.nullish(), company: company.nullish(),
companyId: z.string(), companyId: z.string(),
id: z.string().optional(), id: z.string().optional(),
jobType: z.string(), jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)),
location: z.string(), location: z.string(),
monthYearReceived: z.date(), monthYearReceived: z.date(),
negotiationStrategy: z.string(), negotiationStrategy: z.string(),
@ -73,7 +77,10 @@ const experience = z.object({
companyId: z.string().nullish(), companyId: z.string().nullish(),
durationInMonths: z.number().nullish(), durationInMonths: z.number().nullish(),
id: z.string().optional(), id: z.string().optional(),
jobType: z.string().nullish(), jobType: z
.string()
.regex(createValidationRegex(Object.keys(JobType), null))
.nullish(),
level: z.string().nullish(), level: z.string().nullish(),
location: z.string().nullish(), location: z.string().nullish(),
monthlySalary: valuation.nullish(), monthlySalary: valuation.nullish(),
@ -94,15 +101,6 @@ const education = z.object({
type: z.string().nullish(), type: z.string().nullish(),
}); });
// Const reply = z.object({
// createdAt: z.date().nullish(),
// id: z.string().optional(),
// messages: z.string().nullish(),
// profileId: z.string().nullish(),
// replyingToId: z.string().nullish(),
// userId: z.string().nullish(),
// });
export const offersProfileRouter = createRouter() export const offersProfileRouter = createRouter()
.query('listOne', { .query('listOne', {
input: z.object({ input: z.object({
@ -282,11 +280,11 @@ export const offersProfileRouter = createRouter()
})), })),
}, },
experiences: { experiences: {
create: input.background.experiences.map((x) => { create: input.background.experiences.map(async (x) => {
if ( if (
x.jobType === 'FULLTIME' && x.jobType === JobType.FULLTIME &&
x.totalCompensation?.currency !== undefined && x.totalCompensation?.currency != null &&
x.totalCompensation.value !== undefined x.totalCompensation?.value != null
) { ) {
if (x.companyId) { if (x.companyId) {
return { return {
@ -302,8 +300,14 @@ export const offersProfileRouter = createRouter()
title: x.title, title: x.title,
totalCompensation: { totalCompensation: {
create: { create: {
currency: x.totalCompensation?.currency, baseCurrency: baseCurrencyString,
value: x.totalCompensation?.value, baseValue: await convert(
x.totalCompensation.value,
x.totalCompensation.currency,
baseCurrencyString,
),
currency: x.totalCompensation.currency,
value: x.totalCompensation.value,
}, },
}, },
}; };
@ -312,20 +316,27 @@ export const offersProfileRouter = createRouter()
durationInMonths: x.durationInMonths, durationInMonths: x.durationInMonths,
jobType: x.jobType, jobType: x.jobType,
level: x.level, level: x.level,
location: x.location,
specialization: x.specialization, specialization: x.specialization,
title: x.title, title: x.title,
totalCompensation: { totalCompensation: {
create: { create: {
currency: x.totalCompensation?.currency, baseCurrency: baseCurrencyString,
value: x.totalCompensation?.value, baseValue: await convert(
x.totalCompensation.value,
x.totalCompensation.currency,
baseCurrencyString,
),
currency: x.totalCompensation.currency,
value: x.totalCompensation.value,
}, },
}, },
}; };
} }
if ( if (
x.jobType === 'INTERN' && x.jobType === JobType.INTERN &&
x.monthlySalary?.currency !== undefined && x.monthlySalary?.currency != null &&
x.monthlySalary.value !== undefined x.monthlySalary?.value != null
) { ) {
if (x.companyId) { if (x.companyId) {
return { return {
@ -338,8 +349,14 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType, jobType: x.jobType,
monthlySalary: { monthlySalary: {
create: { create: {
currency: x.monthlySalary?.currency, baseCurrency: baseCurrencyString,
value: x.monthlySalary?.value, baseValue: await convert(
x.monthlySalary.value,
x.monthlySalary.currency,
baseCurrencyString,
),
currency: x.monthlySalary.currency,
value: x.monthlySalary.value,
}, },
}, },
specialization: x.specialization, specialization: x.specialization,
@ -351,8 +368,14 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType, jobType: x.jobType,
monthlySalary: { monthlySalary: {
create: { create: {
currency: x.monthlySalary?.currency, baseCurrency: baseCurrencyString,
value: x.monthlySalary?.value, baseValue: await convert(
x.monthlySalary.value,
x.monthlySalary.currency,
baseCurrencyString,
),
currency: x.monthlySalary.currency,
value: x.monthlySalary.value,
}, },
}, },
specialization: x.specialization, specialization: x.specialization,
@ -379,14 +402,15 @@ export const offersProfileRouter = createRouter()
}, },
editToken: token, editToken: token,
offers: { offers: {
create: input.offers.map((x) => { create: await Promise.all(
input.offers.map(async (x) => {
if ( if (
x.jobType === 'INTERN' && x.jobType === JobType.INTERN &&
x.offersIntern && x.offersIntern &&
x.offersIntern.internshipCycle && x.offersIntern.internshipCycle != null &&
x.offersIntern.monthlySalary?.currency && x.offersIntern.monthlySalary?.currency != null &&
x.offersIntern.monthlySalary.value && x.offersIntern.monthlySalary?.value != null &&
x.offersIntern.startYear x.offersIntern.startYear != null
) { ) {
return { return {
comments: x.comments, comments: x.comments,
@ -404,8 +428,14 @@ export const offersProfileRouter = createRouter()
internshipCycle: x.offersIntern.internshipCycle, internshipCycle: x.offersIntern.internshipCycle,
monthlySalary: { monthlySalary: {
create: { create: {
currency: x.offersIntern.monthlySalary?.currency, baseCurrency: baseCurrencyString,
value: x.offersIntern.monthlySalary?.value, baseValue: await convert(
x.offersIntern.monthlySalary.value,
x.offersIntern.monthlySalary.currency,
baseCurrencyString,
),
currency: x.offersIntern.monthlySalary.currency,
value: x.offersIntern.monthlySalary.value,
}, },
}, },
specialization: x.offersIntern.specialization, specialization: x.offersIntern.specialization,
@ -416,17 +446,19 @@ export const offersProfileRouter = createRouter()
}; };
} }
if ( if (
x.jobType === 'FULLTIME' && x.jobType === JobType.FULLTIME &&
x.offersFullTime && x.offersFullTime &&
x.offersFullTime.baseSalary?.currency && x.offersFullTime.baseSalary?.currency != null &&
x.offersFullTime.baseSalary?.value && x.offersFullTime.baseSalary?.value != null &&
x.offersFullTime.bonus?.currency && x.offersFullTime.bonus?.currency != null &&
x.offersFullTime.bonus?.value && x.offersFullTime.bonus?.value != null &&
x.offersFullTime.stocks?.currency && x.offersFullTime.stocks?.currency != null &&
x.offersFullTime.stocks?.value && x.offersFullTime.stocks?.value != null &&
x.offersFullTime.totalCompensation?.currency && x.offersFullTime.totalCompensation?.currency != null &&
x.offersFullTime.totalCompensation?.value && x.offersFullTime.totalCompensation?.value != null &&
x.offersFullTime.level x.offersFullTime.level != null &&
x.offersFullTime.title != null &&
x.offersFullTime.specialization != null
) { ) {
return { return {
comments: x.comments, comments: x.comments,
@ -443,30 +475,54 @@ export const offersProfileRouter = createRouter()
create: { create: {
baseSalary: { baseSalary: {
create: { create: {
currency: x.offersFullTime.baseSalary?.currency, baseCurrency: baseCurrencyString,
value: x.offersFullTime.baseSalary?.value, baseValue: await convert(
x.offersFullTime.baseSalary.value,
x.offersFullTime.baseSalary.currency,
baseCurrencyString,
),
currency: x.offersFullTime.baseSalary.currency,
value: x.offersFullTime.baseSalary.value,
}, },
}, },
bonus: { bonus: {
create: { create: {
currency: x.offersFullTime.bonus?.currency, baseCurrency: baseCurrencyString,
value: x.offersFullTime.bonus?.value, baseValue: await convert(
x.offersFullTime.bonus.value,
x.offersFullTime.bonus.currency,
baseCurrencyString,
),
currency: x.offersFullTime.bonus.currency,
value: x.offersFullTime.bonus.value,
}, },
}, },
level: x.offersFullTime.level, level: x.offersFullTime.level,
specialization: x.offersFullTime.specialization, specialization: x.offersFullTime.specialization,
stocks: { stocks: {
create: { create: {
currency: x.offersFullTime.stocks?.currency, baseCurrency: baseCurrencyString,
value: x.offersFullTime.stocks?.value, baseValue: await convert(
x.offersFullTime.stocks.value,
x.offersFullTime.stocks.currency,
baseCurrencyString,
),
currency: x.offersFullTime.stocks.currency,
value: x.offersFullTime.stocks.value,
}, },
}, },
title: x.offersFullTime.title, title: x.offersFullTime.title,
totalCompensation: { totalCompensation: {
create: { create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
x.offersFullTime.totalCompensation.value,
x.offersFullTime.totalCompensation.currency,
baseCurrencyString,
),
currency: currency:
x.offersFullTime.totalCompensation?.currency, x.offersFullTime.totalCompensation.currency,
value: x.offersFullTime.totalCompensation?.value, value: x.offersFullTime.totalCompensation.value,
}, },
}, },
}, },
@ -480,6 +536,7 @@ export const offersProfileRouter = createRouter()
message: 'Missing fields.', message: 'Missing fields.',
}); });
}), }),
),
}, },
profileName: randomUUID().substring(0, 10), profileName: randomUUID().substring(0, 10),
}, },
@ -510,7 +567,7 @@ export const offersProfileRouter = createRouter()
return deletedProfile.id; return deletedProfile.id;
} }
// TODO: Throw 401
throw new trpc.TRPCError({ throw new trpc.TRPCError({
code: 'UNAUTHORIZED', code: 'UNAUTHORIZED',
message: 'Invalid token.', message: 'Invalid token.',
@ -535,7 +592,6 @@ export const offersProfileRouter = createRouter()
totalYoe: z.number(), totalYoe: z.number(),
}), }),
createdAt: z.string().optional(), createdAt: z.string().optional(),
// Discussion: z.array(reply),
id: z.string(), id: z.string(),
isEditable: z.boolean().nullish(), isEditable: z.boolean().nullish(),
offers: z.array(offer), offers: z.array(offer),
@ -573,19 +629,21 @@ export const offersProfileRouter = createRouter()
}); });
// Delete educations // Delete educations
const educationsId = (await ctx.prisma.offersEducation.findMany({ const educationsId = (
await ctx.prisma.offersEducation.findMany({
where: { where: {
backgroundId: input.background.id backgroundId: input.background.id,
} },
})).map((x) => x.id) })
).map((x) => x.id);
for (const id of educationsId) { for (const id of educationsId) {
if (!input.background.educations.map((x) => x.id).includes(id)) { if (!input.background.educations.map((x) => x.id).includes(id)) {
await ctx.prisma.offersEducation.delete({ await ctx.prisma.offersEducation.delete({
where: { where: {
id id,
} },
}) });
} }
} }
@ -626,19 +684,21 @@ export const offersProfileRouter = createRouter()
} }
// Delete experiences // Delete experiences
const experiencesId = (await ctx.prisma.offersExperience.findMany({ const experiencesId = (
await ctx.prisma.offersExperience.findMany({
where: { where: {
backgroundId: input.background.id backgroundId: input.background.id,
} },
})).map((x) => x.id) })
).map((x) => x.id);
for (const id of experiencesId) { for (const id of experiencesId) {
if (!input.background.experiences.map((x) => x.id).includes(id)) { if (!input.background.experiences.map((x) => x.id).includes(id)) {
await ctx.prisma.offersExperience.delete({ await ctx.prisma.offersExperience.delete({
where: { where: {
id id,
} },
}) });
} }
} }
@ -660,6 +720,12 @@ export const offersProfileRouter = createRouter()
if (exp.monthlySalary) { if (exp.monthlySalary) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency, currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value, value: exp.monthlySalary.value,
}, },
@ -672,6 +738,12 @@ export const offersProfileRouter = createRouter()
if (exp.totalCompensation) { if (exp.totalCompensation) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency, currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value, value: exp.totalCompensation.value,
}, },
@ -682,11 +754,9 @@ export const offersProfileRouter = createRouter()
} }
} else if (!exp.id) { } else if (!exp.id) {
// Create new experience // Create new experience
if ( if (exp.jobType === JobType.FULLTIME) {
exp.jobType === 'FULLTIME' && if (exp.totalCompensation?.currency != null &&
exp.totalCompensation?.currency !== undefined && exp.totalCompensation?.value != null) {
exp.totalCompensation.value !== undefined
) {
if (exp.companyId) { if (exp.companyId) {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
@ -700,12 +770,19 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
location: exp.location,
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
totalCompensation: { totalCompensation: {
create: { create: {
currency: exp.totalCompensation?.currency, baseCurrency: baseCurrencyString,
value: exp.totalCompensation?.value, baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
}, },
}, },
}, },
@ -723,12 +800,19 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
level: exp.level, level: exp.level,
location: exp.location,
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
totalCompensation: { totalCompensation: {
create: { create: {
currency: exp.totalCompensation?.currency, baseCurrency: baseCurrencyString,
value: exp.totalCompensation?.value, baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
}, },
}, },
}, },
@ -739,11 +823,51 @@ export const offersProfileRouter = createRouter()
}, },
}); });
} }
} else if ( } else if (exp.companyId) {
exp.jobType === 'INTERN' && await ctx.prisma.offersBackground.update({
exp.monthlySalary?.currency !== undefined && data: {
exp.monthlySalary.value !== undefined experiences: {
) { create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
specialization: exp.specialization,
title: exp.title,
},
},
},
where: {
id: input.background.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
experiences: {
create: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
specialization: exp.specialization,
title: exp.title,
},
},
},
where: {
id: input.background.id,
},
});
}
} else if (exp.jobType === JobType.INTERN) {
if (exp.monthlySalary?.currency != null &&
exp.monthlySalary?.value != null) {
if (exp.companyId) { if (exp.companyId) {
await ctx.prisma.offersBackground.update({ await ctx.prisma.offersBackground.update({
data: { data: {
@ -756,10 +880,17 @@ export const offersProfileRouter = createRouter()
}, },
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location,
monthlySalary: { monthlySalary: {
create: { create: {
currency: exp.monthlySalary?.currency, baseCurrency: baseCurrencyString,
value: exp.monthlySalary?.value, baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
}, },
}, },
specialization: exp.specialization, specialization: exp.specialization,
@ -778,12 +909,59 @@ export const offersProfileRouter = createRouter()
create: { create: {
durationInMonths: exp.durationInMonths, durationInMonths: exp.durationInMonths,
jobType: exp.jobType, jobType: exp.jobType,
location: exp.location,
monthlySalary: { monthlySalary: {
create: { create: {
currency: exp.monthlySalary?.currency, baseCurrency: baseCurrencyString,
value: exp.monthlySalary?.value, baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
specialization: exp.specialization,
title: exp.title,
},
},
},
where: {
id: input.background.id,
},
});
}
} else if (exp.companyId) {
await ctx.prisma.offersBackground.update({
data: {
experiences: {
create: {
company: {
connect: {
id: exp.companyId,
},
},
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
specialization: exp.specialization,
title: exp.title,
}, },
}, },
},
where: {
id: input.background.id,
},
});
} else {
await ctx.prisma.offersBackground.update({
data: {
experiences: {
create: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
specialization: exp.specialization, specialization: exp.specialization,
title: exp.title, title: exp.title,
}, },
@ -799,19 +977,21 @@ export const offersProfileRouter = createRouter()
} }
// Delete specific yoes // Delete specific yoes
const yoesId = (await ctx.prisma.offersSpecificYoe.findMany({ const yoesId = (
await ctx.prisma.offersSpecificYoe.findMany({
where: { where: {
backgroundId: input.background.id backgroundId: input.background.id,
} },
})).map((x) => x.id) })
).map((x) => x.id);
for (const id of yoesId) { for (const id of yoesId) {
if (!input.background.specificYoes.map((x) => x.id).includes(id)) { if (!input.background.specificYoes.map((x) => x.id).includes(id)) {
await ctx.prisma.offersSpecificYoe.delete({ await ctx.prisma.offersSpecificYoe.delete({
where: { where: {
id id,
} },
}) });
} }
} }
@ -845,19 +1025,21 @@ export const offersProfileRouter = createRouter()
} }
// Delete specific offers // Delete specific offers
const offers = (await ctx.prisma.offersOffer.findMany({ const offers = (
await ctx.prisma.offersOffer.findMany({
where: { where: {
profileId: input.id profileId: input.id,
} },
})).map((x) => x.id) })
).map((x) => x.id);
for (const id of offers) { for (const id of offers) {
if (!input.offers.map((x) => x.id).includes(id)) { if (!input.offers.map((x) => x.id).includes(id)) {
await ctx.prisma.offersOffer.delete({ await ctx.prisma.offersOffer.delete({
where: { where: {
id id,
} },
}) });
} }
} }
@ -869,6 +1051,10 @@ export const offersProfileRouter = createRouter()
data: { data: {
comments: offerToUpdate.comments, comments: offerToUpdate.comments,
companyId: offerToUpdate.companyId, companyId: offerToUpdate.companyId,
jobType:
offerToUpdate.jobType === JobType.FULLTIME
? JobType.FULLTIME
: JobType.INTERN,
location: offerToUpdate.location, location: offerToUpdate.location,
monthYearReceived: offerToUpdate.monthYearReceived, monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy, negotiationStrategy: offerToUpdate.negotiationStrategy,
@ -878,21 +1064,7 @@ export const offersProfileRouter = createRouter()
}, },
}); });
if ( if (offerToUpdate.offersIntern?.monthlySalary != null) {
offerToUpdate.jobType === 'INTERN' ||
offerToUpdate.jobType === 'FULLTIME'
) {
await ctx.prisma.offersOffer.update({
data: {
jobType: offerToUpdate.jobType,
},
where: {
id: offerToUpdate.id,
},
});
}
if (offerToUpdate.offersIntern?.monthlySalary) {
await ctx.prisma.offersIntern.update({ await ctx.prisma.offersIntern.update({
data: { data: {
internshipCycle: internshipCycle:
@ -907,6 +1079,12 @@ export const offersProfileRouter = createRouter()
}); });
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersIntern.monthlySalary.value,
offerToUpdate.offersIntern.monthlySalary.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersIntern.monthlySalary.currency, currency: offerToUpdate.offersIntern.monthlySalary.currency,
value: offerToUpdate.offersIntern.monthlySalary.value, value: offerToUpdate.offersIntern.monthlySalary.value,
}, },
@ -916,7 +1094,7 @@ export const offersProfileRouter = createRouter()
}); });
} }
if (offerToUpdate.offersFullTime?.totalCompensation) { if (offerToUpdate.offersFullTime?.totalCompensation != null) {
await ctx.prisma.offersFullTime.update({ await ctx.prisma.offersFullTime.update({
data: { data: {
level: offerToUpdate.offersFullTime.level ?? undefined, level: offerToUpdate.offersFullTime.level ?? undefined,
@ -927,9 +1105,15 @@ export const offersProfileRouter = createRouter()
id: offerToUpdate.offersFullTime.id, id: offerToUpdate.offersFullTime.id,
}, },
}); });
if (offerToUpdate.offersFullTime.baseSalary) { if (offerToUpdate.offersFullTime.baseSalary != null) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.baseSalary.value,
offerToUpdate.offersFullTime.baseSalary.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.baseSalary.currency, currency: offerToUpdate.offersFullTime.baseSalary.currency,
value: offerToUpdate.offersFullTime.baseSalary.value, value: offerToUpdate.offersFullTime.baseSalary.value,
}, },
@ -941,6 +1125,12 @@ export const offersProfileRouter = createRouter()
if (offerToUpdate.offersFullTime.bonus) { if (offerToUpdate.offersFullTime.bonus) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.bonus.value,
offerToUpdate.offersFullTime.bonus.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.bonus.currency, currency: offerToUpdate.offersFullTime.bonus.currency,
value: offerToUpdate.offersFullTime.bonus.value, value: offerToUpdate.offersFullTime.bonus.value,
}, },
@ -952,6 +1142,12 @@ export const offersProfileRouter = createRouter()
if (offerToUpdate.offersFullTime.stocks) { if (offerToUpdate.offersFullTime.stocks) {
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.stocks.value,
offerToUpdate.offersFullTime.stocks.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.stocks.currency, currency: offerToUpdate.offersFullTime.stocks.currency,
value: offerToUpdate.offersFullTime.stocks.value, value: offerToUpdate.offersFullTime.stocks.value,
}, },
@ -962,6 +1158,12 @@ export const offersProfileRouter = createRouter()
} }
await ctx.prisma.offersCurrency.update({ await ctx.prisma.offersCurrency.update({
data: { data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.totalCompensation.value,
offerToUpdate.offersFullTime.totalCompensation.currency,
baseCurrencyString,
),
currency: currency:
offerToUpdate.offersFullTime.totalCompensation.currency, offerToUpdate.offersFullTime.totalCompensation.currency,
value: offerToUpdate.offersFullTime.totalCompensation.value, value: offerToUpdate.offersFullTime.totalCompensation.value,
@ -974,12 +1176,12 @@ export const offersProfileRouter = createRouter()
} else { } else {
// Create new offer // Create new offer
if ( if (
offerToUpdate.jobType === 'INTERN' && offerToUpdate.jobType === JobType.INTERN &&
offerToUpdate.offersIntern && offerToUpdate.offersIntern &&
offerToUpdate.offersIntern.internshipCycle && offerToUpdate.offersIntern.internshipCycle != null &&
offerToUpdate.offersIntern.monthlySalary?.currency && offerToUpdate.offersIntern.monthlySalary?.currency != null &&
offerToUpdate.offersIntern.monthlySalary.value && offerToUpdate.offersIntern.monthlySalary?.value != null &&
offerToUpdate.offersIntern.startYear offerToUpdate.offersIntern.startYear != null
) { ) {
await ctx.prisma.offersProfile.update({ await ctx.prisma.offersProfile.update({
data: { data: {
@ -1001,11 +1203,18 @@ export const offersProfileRouter = createRouter()
offerToUpdate.offersIntern.internshipCycle, offerToUpdate.offersIntern.internshipCycle,
monthlySalary: { monthlySalary: {
create: { create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersIntern.monthlySalary.value,
offerToUpdate.offersIntern.monthlySalary
.currency,
baseCurrencyString,
),
currency: currency:
offerToUpdate.offersIntern.monthlySalary offerToUpdate.offersIntern.monthlySalary
?.currency, .currency,
value: value:
offerToUpdate.offersIntern.monthlySalary?.value, offerToUpdate.offersIntern.monthlySalary.value,
}, },
}, },
specialization: specialization:
@ -1023,17 +1232,18 @@ export const offersProfileRouter = createRouter()
}); });
} }
if ( if (
offerToUpdate.jobType === 'FULLTIME' && offerToUpdate.jobType === JobType.FULLTIME &&
offerToUpdate.offersFullTime && offerToUpdate.offersFullTime &&
offerToUpdate.offersFullTime.baseSalary?.currency && offerToUpdate.offersFullTime.baseSalary?.currency != null &&
offerToUpdate.offersFullTime.baseSalary?.value && offerToUpdate.offersFullTime.baseSalary?.value != null &&
offerToUpdate.offersFullTime.bonus?.currency && offerToUpdate.offersFullTime.bonus?.currency != null &&
offerToUpdate.offersFullTime.bonus?.value && offerToUpdate.offersFullTime.bonus?.value != null &&
offerToUpdate.offersFullTime.stocks?.currency && offerToUpdate.offersFullTime.stocks?.currency != null &&
offerToUpdate.offersFullTime.stocks?.value && offerToUpdate.offersFullTime.stocks?.value != null &&
offerToUpdate.offersFullTime.totalCompensation?.currency && offerToUpdate.offersFullTime.totalCompensation?.currency !=
offerToUpdate.offersFullTime.totalCompensation?.value && null &&
offerToUpdate.offersFullTime.level offerToUpdate.offersFullTime.totalCompensation?.value != null &&
offerToUpdate.offersFullTime.level != null
) { ) {
await ctx.prisma.offersProfile.update({ await ctx.prisma.offersProfile.update({
data: { data: {
@ -1053,18 +1263,31 @@ export const offersProfileRouter = createRouter()
create: { create: {
baseSalary: { baseSalary: {
create: { create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.baseSalary.value,
offerToUpdate.offersFullTime.baseSalary
.currency,
baseCurrencyString,
),
currency: currency:
offerToUpdate.offersFullTime.baseSalary offerToUpdate.offersFullTime.baseSalary
?.currency, .currency,
value: value:
offerToUpdate.offersFullTime.baseSalary?.value, offerToUpdate.offersFullTime.baseSalary.value,
}, },
}, },
bonus: { bonus: {
create: { create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.bonus.value,
offerToUpdate.offersFullTime.bonus.currency,
baseCurrencyString,
),
currency: currency:
offerToUpdate.offersFullTime.bonus?.currency, offerToUpdate.offersFullTime.bonus.currency,
value: offerToUpdate.offersFullTime.bonus?.value, value: offerToUpdate.offersFullTime.bonus.value,
}, },
}, },
level: offerToUpdate.offersFullTime.level, level: offerToUpdate.offersFullTime.level,
@ -1072,20 +1295,34 @@ export const offersProfileRouter = createRouter()
offerToUpdate.offersFullTime.specialization, offerToUpdate.offersFullTime.specialization,
stocks: { stocks: {
create: { create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.stocks.value,
offerToUpdate.offersFullTime.stocks.currency,
baseCurrencyString,
),
currency: currency:
offerToUpdate.offersFullTime.stocks?.currency, offerToUpdate.offersFullTime.stocks.currency,
value: offerToUpdate.offersFullTime.stocks?.value, value: offerToUpdate.offersFullTime.stocks.value,
}, },
}, },
title: offerToUpdate.offersFullTime.title, title: offerToUpdate.offersFullTime.title,
totalCompensation: { totalCompensation: {
create: { create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.totalCompensation
.value,
offerToUpdate.offersFullTime.totalCompensation
.currency,
baseCurrencyString,
),
currency: currency:
offerToUpdate.offersFullTime.totalCompensation offerToUpdate.offersFullTime.totalCompensation
?.currency, .currency,
value: value:
offerToUpdate.offersFullTime.totalCompensation offerToUpdate.offersFullTime.totalCompensation
?.value, .value,
}, },
}, },
}, },
@ -1102,46 +1339,6 @@ export const offersProfileRouter = createRouter()
} }
const result = await ctx.prisma.offersProfile.findFirst({ const result = await ctx.prisma.offersProfile.findFirst({
include: {
background: {
include: {
educations: true,
experiences: {
include: {
company: true,
monthlySalary: true,
totalCompensation: true,
},
},
specificYoes: true,
},
},
discussion: {
include: {
replies: true,
replyingTo: true,
user: true,
},
},
offers: {
include: {
company: true,
offersFullTime: {
include: {
baseSalary: true,
bonus: true,
stocks: true,
totalCompensation: true,
},
},
offersIntern: {
include: {
monthlySalary: true,
},
},
},
},
},
where: { where: {
id: input.id, id: input.id,
}, },

@ -5,9 +5,25 @@ import {
dashboardOfferDtoMapper, dashboardOfferDtoMapper,
getOffersResponseMapper, getOffersResponseMapper,
} from '~/mappers/offers-mappers'; } from '~/mappers/offers-mappers';
import { convertWithDate } from '~/utils/offers/currency/currencyExchange';
import { Currency } from '~/utils/offers/currency/CurrencyEnum';
import { createValidationRegex } from '~/utils/offers/zodRegex';
import { createRouter } from '../context'; import { createRouter } from '../context';
const getOrder = (prefix: string) => {
if (prefix === '+') {
return 'asc';
}
return 'desc';
};
const sortingKeysMap = {
monthYearReceived: 'monthYearReceived',
totalCompensation: 'totalCompensation',
totalYoe: 'totalYoe',
};
const yoeCategoryMap: Record<number, string> = { const yoeCategoryMap: Record<number, string> = {
0: 'Internship', 0: 'Internship',
1: 'Fresh Grad', 1: 'Fresh Grad',
@ -25,19 +41,10 @@ const getYoeRange = (yoeCategory: number) => {
: null; // Internship : null; // Internship
}; };
const ascOrder = '+';
const descOrder = '-';
const sortingKeys = ['monthYearReceived', 'totalCompensation', 'totalYoe'];
const createSortByValidationRegex = () => {
const startsWithPlusOrMinusOnly = '^[+-]{1}';
const sortingKeysRegex = sortingKeys.join('|');
return new RegExp(startsWithPlusOrMinusOnly + '(' + sortingKeysRegex + ')');
};
export const offersRouter = createRouter().query('list', { export const offersRouter = createRouter().query('list', {
input: z.object({ input: z.object({
companyId: z.string().nullish(), companyId: z.string().nullish(),
currency: z.string().nullish(),
dateEnd: z.date().nullish(), dateEnd: z.date().nullish(),
dateStart: z.date().nullish(), dateStart: z.date().nullish(),
limit: z.number().positive(), limit: z.number().positive(),
@ -45,7 +52,10 @@ export const offersRouter = createRouter().query('list', {
offset: z.number().nonnegative(), offset: z.number().nonnegative(),
salaryMax: z.number().nonnegative().nullish(), salaryMax: z.number().nonnegative().nullish(),
salaryMin: z.number().nonnegative().nullish(), salaryMin: z.number().nonnegative().nullish(),
sortBy: z.string().regex(createSortByValidationRegex()).nullish(), sortBy: z
.string()
.regex(createValidationRegex(Object.keys(sortingKeysMap), '[+-]{1}'))
.nullish(),
title: z.string().nullish(), title: z.string().nullish(),
yoeCategory: z.number().min(0).max(3), yoeCategory: z.number().min(0).max(3),
yoeMax: z.number().max(100).nullish(), yoeMax: z.number().max(100).nullish(),
@ -56,6 +66,13 @@ export const offersRouter = createRouter().query('list', {
const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe; const yoeMin = input.yoeMin ? input.yoeMin : yoeRange?.minYoe;
const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe; const yoeMax = input.yoeMax ? input.yoeMax : yoeRange?.maxYoe;
if (!input.sortBy) {
input.sortBy = '-' + sortingKeysMap.monthYearReceived;
}
const order = getOrder(input.sortBy.charAt(0));
const sortingKey = input.sortBy.substring(1);
let data = !yoeRange let data = !yoeRange
? await ctx.prisma.offersOffer.findMany({ ? await ctx.prisma.offersOffer.findMany({
// Internship // Internship
@ -80,21 +97,84 @@ export const offersRouter = createRouter().query('list', {
}, },
}, },
}, },
orderBy:
sortingKey === sortingKeysMap.monthYearReceived
? {
monthYearReceived: order,
}
: sortingKey === sortingKeysMap.totalCompensation
? {
offersIntern: {
monthlySalary: {
baseValue: order,
},
},
}
: sortingKey === sortingKeysMap.totalYoe
? {
profile: {
background: {
totalYoe: order,
},
},
}
: undefined,
where: { where: {
AND: [ AND: [
{ {
location: input.location, location:
input.location.length === 0 ? undefined : input.location,
}, },
{ {
offersIntern: { offersIntern: {
isNot: null, isNot: null,
}, },
}, },
{
offersIntern: {
title:
input.title && input.title.length !== 0
? input.title
: undefined,
},
},
{
offersIntern: {
monthlySalary: {
baseValue: {
gte: input.salaryMin ?? undefined,
lte: input.salaryMax ?? undefined,
},
},
},
},
{ {
offersFullTime: { offersFullTime: {
is: null, is: null,
}, },
}, },
{
companyId:
input.companyId && input.companyId.length !== 0
? input.companyId
: undefined,
},
{
profile: {
background: {
totalYoe: {
gte: yoeMin,
lte: yoeMax,
},
},
},
},
{
monthYearReceived: {
gte: input.dateStart ?? undefined,
lte: input.dateEnd ?? undefined,
},
},
], ],
}, },
}) })
@ -121,10 +201,33 @@ export const offersRouter = createRouter().query('list', {
}, },
}, },
}, },
orderBy:
sortingKey === sortingKeysMap.monthYearReceived
? {
monthYearReceived: order,
}
: sortingKey === sortingKeysMap.totalCompensation
? {
offersFullTime: {
totalCompensation: {
baseValue: order,
},
},
}
: sortingKey === sortingKeysMap.totalYoe
? {
profile: {
background: {
totalYoe: order,
},
},
}
: undefined,
where: { where: {
AND: [ AND: [
{ {
location: input.location, location:
input.location.length === 0 ? undefined : input.location,
}, },
{ {
offersIntern: { offersIntern: {
@ -136,6 +239,30 @@ export const offersRouter = createRouter().query('list', {
isNot: null, isNot: null,
}, },
}, },
{
offersFullTime: {
title:
input.title && input.title.length !== 0
? input.title
: undefined,
},
},
{
offersFullTime: {
totalCompensation: {
baseValue: {
gte: input.salaryMin ?? undefined,
lte: input.salaryMax ?? undefined,
},
},
},
},
{
companyId:
input.companyId && input.companyId.length !== 0
? input.companyId
: undefined,
},
{ {
profile: { profile: {
background: { background: {
@ -146,165 +273,70 @@ export const offersRouter = createRouter().query('list', {
}, },
}, },
}, },
{
monthYearReceived: {
gte: input.dateStart ?? undefined,
lte: input.dateEnd ?? undefined,
},
},
], ],
}, },
}); });
// FILTERING // CONVERTING
data = data.filter((offer) => { const currency = input.currency?.toUpperCase();
let validRecord = true; if (currency != null && currency in Currency) {
data = await Promise.all(
if (input.companyId && input.companyId.length !== 0) { data.map(async (offer) => {
validRecord = validRecord && offer.company.id === input.companyId; if (offer.offersFullTime?.totalCompensation != null) {
} offer.offersFullTime.totalCompensation.value =
await convertWithDate(
if (input.title && input.title.length !== 0) { offer.offersFullTime.totalCompensation.value,
validRecord = offer.offersFullTime.totalCompensation.currency,
validRecord && currency,
(offer.offersFullTime?.title === input.title || offer.offersFullTime.totalCompensation.updatedAt,
offer.offersIntern?.title === input.title);
}
if (
input.dateStart &&
input.dateEnd &&
input.dateStart.getTime() <= input.dateEnd.getTime()
) {
validRecord =
validRecord &&
offer.monthYearReceived.getTime() >= input.dateStart.getTime() &&
offer.monthYearReceived.getTime() <= input.dateEnd.getTime();
}
if (input.salaryMin != null || input.salaryMax != null) {
const salary = offer.offersFullTime?.totalCompensation.value
? offer.offersFullTime?.totalCompensation.value
: offer.offersIntern?.monthlySalary.value;
if (salary == null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Total Compensation or Salary not found',
});
}
if (input.salaryMin != null) {
validRecord = validRecord && salary >= input.salaryMin;
}
if (input.salaryMax != null) {
validRecord = validRecord && salary <= input.salaryMax;
}
}
return validRecord;
});
// SORTING
data = data.sort((offer1, offer2) => {
const defaultReturn =
offer2.monthYearReceived.getTime() - offer1.monthYearReceived.getTime();
if (!input.sortBy) {
return defaultReturn;
}
const order = input.sortBy.charAt(0);
const sortingKey = input.sortBy.substring(1);
if (order === ascOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer1.monthYearReceived.getTime() -
offer2.monthYearReceived.getTime()
); );
} offer.offersFullTime.totalCompensation.currency = currency;
offer.offersFullTime.baseSalary.value = await convertWithDate(
if (sortingKey === 'totalCompensation') { offer.offersFullTime.baseSalary.value,
const salary1 = offer1.offersFullTime?.totalCompensation.value offer.offersFullTime.baseSalary.currency,
? offer1.offersFullTime?.totalCompensation.value currency,
: offer1.offersIntern?.monthlySalary.value; offer.offersFullTime.baseSalary.updatedAt,
const salary2 = offer2.offersFullTime?.totalCompensation.value
? offer2.offersFullTime?.totalCompensation.value
: offer2.offersIntern?.monthlySalary.value;
if (salary1 == null || salary2 == null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Total Compensation or Salary not found',
});
}
return salary1 - salary2;
}
if (sortingKey === 'totalYoe') {
const yoe1 = offer1.profile.background?.totalYoe;
const yoe2 = offer2.profile.background?.totalYoe;
if (yoe1 == null || yoe2 == null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Total years of experience not found',
});
}
return yoe1 - yoe2;
}
return defaultReturn;
})();
}
if (order === descOrder) {
return (() => {
if (sortingKey === 'monthYearReceived') {
return (
offer2.monthYearReceived.getTime() -
offer1.monthYearReceived.getTime()
); );
} offer.offersFullTime.baseSalary.currency = currency;
offer.offersFullTime.stocks.value = await convertWithDate(
if (sortingKey === 'totalCompensation') { offer.offersFullTime.stocks.value,
const salary1 = offer1.offersFullTime?.totalCompensation.value offer.offersFullTime.stocks.currency,
? offer1.offersFullTime?.totalCompensation.value currency,
: offer1.offersIntern?.monthlySalary.value; offer.offersFullTime.stocks.updatedAt,
);
const salary2 = offer2.offersFullTime?.totalCompensation.value offer.offersFullTime.stocks.currency = currency;
? offer2.offersFullTime?.totalCompensation.value offer.offersFullTime.bonus.value = await convertWithDate(
: offer2.offersIntern?.monthlySalary.value; offer.offersFullTime.bonus.value,
offer.offersFullTime.bonus.currency,
if (salary1 == null || salary2 == null) { 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,
offer.offersIntern.monthlySalary.currency,
currency,
offer.offersIntern.monthlySalary.updatedAt,
);
offer.offersIntern.monthlySalary.currency = currency;
} else {
throw new TRPCError({ throw new TRPCError({
code: 'NOT_FOUND', code: 'NOT_FOUND',
message: 'Total Compensation or Salary not found', message: 'Total Compensation or Salary not found',
}); });
} }
return salary2 - salary1; return offer;
} }),
);
if (sortingKey === 'totalYoe') {
const yoe1 = offer1.profile.background?.totalYoe;
const yoe2 = offer2.profile.background?.totalYoe;
if (yoe1 == null || yoe2 == null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Total years of experience not found',
});
}
return yoe2 - yoe1;
}
return defaultReturn;
})();
} }
return defaultReturn;
});
const startRecordIndex: number = input.limit * input.offset; const startRecordIndex: number = input.limit * input.offset;
const endRecordIndex: number = const endRecordIndex: number =

@ -42,6 +42,8 @@ export type OffersCompany = {
}; };
export type Valuation = { export type Valuation = {
baseCurrency: string;
baseValue: number;
currency: string; currency: string;
value: number; value: number;
}; };

@ -1,167 +1,170 @@
// eslint-disable-next-line no-shadow // eslint-disable-next-line no-shadow
export enum Currency { export enum Currency {
AED = 'AED', // United Arab Emirates Dirham AED = "AED", // 'UNITED ARAB EMIRATES DIRHAM'
AFN = 'AFN', // Afghanistan Afghani AFN = "AFN", // 'AFGHAN AFGHANI'
ALL = 'ALL', // Albania Lek ALL = "ALL", // 'ALBANIAN LEK'
AMD = 'AMD', // Armenia Dram AMD = "AMD", // 'ARMENIAN DRAM'
ANG = 'ANG', // Netherlands Antilles Guilder ANG = "ANG", // 'NETHERLANDS ANTILLEAN GUILDER'
AOA = 'AOA', // Angola Kwanza AOA = "AOA", // 'ANGOLAN KWANZA'
ARS = 'ARS', // Argentina Peso ARS = "ARS", // 'ARGENTINE PESO'
AUD = 'AUD', // Australia Dollar AUD = "AUD", // 'AUSTRALIAN DOLLAR'
AWG = 'AWG', // Aruba Guilder AWG = "AWG", // 'ARUBAN FLORIN'
AZN = 'AZN', // Azerbaijan New Manat AZN = "AZN", // 'AZERBAIJANI MANAT'
BAM = 'BAM', // Bosnia and Herzegovina Convertible Marka BAM = "BAM", // 'BOSNIA-HERZEGOVINA CONVERTIBLE MARK'
BBD = 'BBD', // Barbados Dollar BBD = "BBD", // 'BAJAN DOLLAR'
BDT = 'BDT', // Bangladesh Taka BDT = "BDT", // 'BANGLADESHI TAKA'
BGN = 'BGN', // Bulgaria Lev BGN = "BGN", // 'BULGARIAN LEV'
BHD = 'BHD', // Bahrain Dinar BHD = "BHD", // 'BAHRAINI DINAR'
BIF = 'BIF', // Burundi Franc BIF = "BIF", // 'BURUNDIAN FRANC'
BMD = 'BMD', // Bermuda Dollar BMD = "BMD", // 'BERMUDAN DOLLAR'
BND = 'BND', // Brunei Darussalam Dollar BND = "BND", // 'BRUNEI DOLLAR'
BOB = 'BOB', // Bolivia Bolíviano BOB = "BOB", // 'BOLIVIAN BOLIVIANO'
BRL = 'BRL', // Brazil Real BRL = "BRL", // 'BRAZILIAN REAL'
BSD = 'BSD', // Bahamas Dollar BSD = "BSD", // 'BAHAMIAN DOLLAR'
BTN = 'BTN', // Bhutan Ngultrum BTN = "BTN", // 'BHUTAN CURRENCY'
BWP = 'BWP', // Botswana Pula BWP = "BWP", // 'BOTSWANAN PULA'
BYR = 'BYR', // Belarus Ruble BYN = "BYN", // 'NEW BELARUSIAN RUBLE'
BZD = 'BZD', // Belize Dollar BYR = "BYR", // 'BELARUSIAN RUBLE'
CAD = 'CAD', // Canada Dollar BZD = "BZD", // 'BELIZE DOLLAR'
CDF = 'CDF', // Congo/Kinshasa Franc CAD = "CAD", // 'CANADIAN DOLLAR'
CHF = 'CHF', // Switzerland Franc CDF = "CDF", // 'CONGOLESE FRANC'
CLP = 'CLP', // Chile Peso CHF = "CHF", // 'SWISS FRANC'
CNY = 'CNY', // China Yuan Renminbi CLF = "CLF", // 'CHILEAN UNIT OF ACCOUNT (UF)'
COP = 'COP', // Colombia Peso CLP = "CLP", // 'CHILEAN PESO'
CRC = 'CRC', // Costa Rica Colon CNY = "CNY", // 'CHINESE YUAN'
CUC = 'CUC', // Cuba Convertible Peso COP = "COP", // 'COLOMBIAN PESO'
CUP = 'CUP', // Cuba Peso CRC = "CRC", // 'COSTA RICAN COLÓN'
CVE = 'CVE', // Cape Verde Escudo CUC = "CUC", // 'CUBAN CONVERTIBLE PESO'
CZK = 'CZK', // Czech Republic Koruna CUP = "CUP", // 'CUBAN PESO'
DJF = 'DJF', // Djibouti Franc CVE = "CVE", // 'CAPE VERDEAN ESCUDO'
DKK = 'DKK', // Denmark Krone CVX = "CVX", // 'CONVEX FINANCE'
DOP = 'DOP', // Dominican Republic Peso CZK = "CZK", // 'CZECH KORUNA'
DZD = 'DZD', // Algeria Dinar DJF = "DJF", // 'DJIBOUTIAN FRANC'
EGP = 'EGP', // Egypt Pound DKK = "DKK", // 'DANISH KRONE'
ERN = 'ERN', // Eritrea Nakfa DOP = "DOP", // 'DOMINICAN PESO'
ETB = 'ETB', // Ethiopia Birr DZD = "DZD", // 'ALGERIAN DINAR'
EUR = 'EUR', // Euro Member Countries EGP = "EGP", // 'EGYPTIAN POUND'
FJD = 'FJD', // Fiji Dollar ERN = "ERN", // 'ERITREAN NAKFA'
FKP = 'FKP', // Falkland Islands (Malvinas) Pound ETB = "ETB", // 'ETHIOPIAN BIRR'
GBP = 'GBP', // United Kingdom Pound ETC = "ETC", // 'ETHEREUM CLASSIC'
GEL = 'GEL', // Georgia Lari EUR = "EUR", // 'EURO'
GGP = 'GGP', // Guernsey Pound FEI = "FEI", // 'FEI USD'
GHS = 'GHS', // Ghana Cedi FJD = "FJD", // 'FIJIAN DOLLAR'
GIP = 'GIP', // Gibraltar Pound FKP = "FKP", // 'FALKLAND ISLANDS POUND'
GMD = 'GMD', // Gambia Dalasi GBP = "GBP", // 'POUND STERLING'
GNF = 'GNF', // Guinea Franc GEL = "GEL", // 'GEORGIAN LARI'
GTQ = 'GTQ', // Guatemala Quetzal GHS = "GHS", // 'GHANAIAN CEDI'
GYD = 'GYD', // Guyana Dollar GIP = "GIP", // 'GIBRALTAR POUND'
HKD = 'HKD', // Hong Kong Dollar GMD = "GMD", // 'GAMBIAN DALASI'
HNL = 'HNL', // Honduras Lempira GNF = "GNF", // 'GUINEAN FRANC'
HRK = 'HRK', // Croatia Kuna GTQ = "GTQ", // 'GUATEMALAN QUETZAL'
HTG = 'HTG', // Haiti Gourde GYD = "GYD", // 'GUYANAESE DOLLAR'
HUF = 'HUF', // Hungary Forint HKD = "HKD", // 'HONG KONG DOLLAR'
IDR = 'IDR', // Indonesia Rupiah HNL = "HNL", // 'HONDURAN LEMPIRA'
ILS = 'ILS', // Israel Shekel HRK = "HRK", // 'CROATIAN KUNA'
IMP = 'IMP', // Isle of Man Pound HTG = "HTG", // 'HAITIAN GOURDE'
INR = 'INR', // India Rupee HUF = "HUF", // 'HUNGARIAN FORINT'
IQD = 'IQD', // Iraq Dinar ICP = "ICP", // 'INTERNET COMPUTER'
IRR = 'IRR', // Iran Rial IDR = "IDR", // 'INDONESIAN RUPIAH'
ISK = 'ISK', // Iceland Krona ILS = "ILS", // 'ISRAELI NEW SHEKEL'
JEP = 'JEP', // Jersey Pound INR = "INR", // 'INDIAN RUPEE'
JMD = 'JMD', // Jamaica Dollar IQD = "IQD", // 'IRAQI DINAR'
JOD = 'JOD', // Jordan Dinar IRR = "IRR", // 'IRANIAN RIAL'
JPY = 'JPY', // Japan Yen ISK = "ISK", // 'ICELANDIC KRÓNA'
KES = 'KES', // Kenya Shilling JEP = "JEP", // 'JERSEY POUND'
KGS = 'KGS', // Kyrgyzstan Som JMD = "JMD", // 'JAMAICAN DOLLAR'
KHR = 'KHR', // Cambodia Riel JOD = "JOD", // 'JORDANIAN DINAR'
KMF = 'KMF', // Comoros Franc JPY = "JPY", // 'JAPANESE YEN'
KPW = 'KPW', // Korea (North) Won KES = "KES", // 'KENYAN SHILLING'
KRW = 'KRW', // Korea (South) Won KGS = "KGS", // 'KYRGYSTANI SOM'
KWD = 'KWD', // Kuwait Dinar KHR = "KHR", // 'CAMBODIAN RIEL'
KYD = 'KYD', // Cayman Islands Dollar KMF = "KMF", // 'COMORIAN FRANC'
KZT = 'KZT', // Kazakhstan Tenge KPW = "KPW", // 'NORTH KOREAN WON'
LAK = 'LAK', // Laos Kip KRW = "KRW", // 'SOUTH KOREAN WON'
LBP = 'LBP', // Lebanon Pound KWD = "KWD", // 'KUWAITI DINAR'
LKR = 'LKR', // Sri Lanka Rupee KYD = "KYD", // 'CAYMAN ISLANDS DOLLAR'
LRD = 'LRD', // Liberia Dollar KZT = "KZT", // 'KAZAKHSTANI TENGE'
LSL = 'LSL', // Lesotho Loti LAK = "LAK", // 'LAOTIAN KIP'
LYD = 'LYD', // Libya Dinar LBP = "LPB", // 'LEBANESE POUND'
MAD = 'MAD', // Morocco Dirham LKR = "LKR", // 'SRI LANKAN RUPEE'
MDL = 'MDL', // Moldova Leu LRD = "LRD", // 'LIBERIAN DOLLAR'
MGA = 'MGA', // Madagascar Ariary LSL = "LSL", // 'LESOTHO LOTI'
MKD = 'MKD', // Macedonia Denar LTL = "LTL", // 'LITHUANIAN LITAS'
MMK = 'MMK', // Myanmar (Burma) Kyat LVL = "LVL", // 'LATVIAN LATS'
MNT = 'MNT', // Mongolia Tughrik LYD = "LYD", // 'LIBYAN DINAR'
MOP = 'MOP', // Macau Pataca MAD = "MAD", // 'MOROCCAN DIRHAM'
MRO = 'MRO', // Mauritania Ouguiya MDL = "MDL", // 'MOLDOVAN LEU'
MUR = 'MUR', // Mauritius Rupee MGA = "MGA", // 'MALAGASY ARIARY'
MVR = 'MVR', // Maldives (Maldive Islands) Rufiyaa MKD = "MKD", // 'MACEDONIAN DENAR'
MWK = 'MWK', // Malawi Kwacha MMK = "MMK", // 'MYANMAR KYAT'
MXN = 'MXN', // Mexico Peso MNT = "MNT", // 'MONGOLIAN TUGRIK'
MYR = 'MYR', // Malaysia Ringgit MOP = "MOP", // 'MACANESE PATACA'
MZN = 'MZN', // Mozambique Metical MRO = "MRO", // 'MAURITANIAN OUGUIYA'
NAD = 'NAD', // Namibia Dollar MUR = "MUR", // 'MAURITIAN RUPEE'
NGN = 'NGN', // Nigeria Naira MVR = "MVR", // 'MALDIVIAN RUFIYAA'
NIO = 'NIO', // Nicaragua Cordoba MWK = "MWK", // 'MALAWIAN KWACHA'
NOK = 'NOK', // Norway Krone MXN = "MXN", // 'MEXICAN PESO'
NPR = 'NPR', // Nepal Rupee MYR = "MYR", // 'MALAYSIAN RINGGIT'
NZD = 'NZD', // New Zealand Dollar MZN = "MZN", // 'MOZAMBICAN METICAL'
OMR = 'OMR', // Oman Rial NAD = "NAD", // 'NAMIBIAN DOLLAR'
PAB = 'PAB', // Panama Balboa NGN = "NGN", // 'NIGERIAN NAIRA'
PEN = 'PEN', // Peru Sol NIO = "NIO", // 'NICARAGUAN CÓRDOBA'
PGK = 'PGK', // Papua New Guinea Kina NOK = "NOK", // 'NORWEGIAN KRONE'
PHP = 'PHP', // Philippines Peso NPR = "NPR", // 'NEPALESE RUPEE'
PKR = 'PKR', // Pakistan Rupee NZD = "NZD", // 'NEW ZEALAND DOLLAR'
PLN = 'PLN', // Poland Zloty OMR = "OMR", // 'OMANI RIAL'
PYG = 'PYG', // Paraguay Guarani ONE = "ONE", // 'MENLO ONE'
QAR = 'QAR', // Qatar Riyal PAB = "PAB", // 'PANAMANIAN BALBOA'
RON = 'RON', // Romania New Leu PGK = "PGK", // 'PAPUA NEW GUINEAN KINA'
RSD = 'RSD', // Serbia Dinar PHP = "PHP", // 'PHILIPPINE PESO'
RUB = 'RUB', // Russia Ruble PKR = "PKR", // 'PAKISTANI RUPEE'
RWF = 'RWF', // Rwanda Franc PLN = "PLN", // 'POLAND ZŁOTY'
SAR = 'SAR', // Saudi Arabia Riyal PYG = "PYG", // 'PARAGUAYAN GUARANI'
SBD = 'SBD', // Solomon Islands Dollar QAR = "QAR", // 'QATARI RIAL'
SCR = 'SCR', // Seychelles Rupee RON = "RON", // 'ROMANIAN LEU'
SDG = 'SDG', // Sudan Pound RSD = "RSD", // 'SERBIAN DINAR'
SEK = 'SEK', // Sweden Krona RUB = "RUB", // 'RUSSIAN RUBLE'
SGD = 'SGD', // Singapore Dollar RWF = "RWF", // 'RWANDAN FRANC'
SHP = 'SHP', // Saint Helena Pound SAR = "SAR", // 'SAUDI RIYAL'
SLL = 'SLL', // Sierra Leone Leone SBD = "SBD", // 'SOLOMON ISLANDS DOLLAR'
SOS = 'SOS', // Somalia Shilling SCR = "SCR", // 'SEYCHELLOIS RUPEE'
SPL = 'SPL', // Seborga Luigino SDG = "SDG", // 'SUDANESE POUND'
SRD = 'SRD', // Suriname Dollar SEK = "SEK", // 'SWEDISH KRONA'
STD = 'STD', // São Tomé and Príncipe Dobra SGD = "SGD", // 'SINGAPORE DOLLAR'
SVC = 'SVC', // El Salvador Colon SHIB = "SHIB", // 'SHIBA INU'
SYP = 'SYP', // Syria Pound SHP = "SHP", // 'SAINT HELENA POUND'
SZL = 'SZL', // Swaziland Lilangeni SLL = "SLL", // 'SIERRA LEONEAN LEONE'
THB = 'THB', // Thailand Baht SOS = "SOS", // 'SOMALI SHILLING'
TJS = 'TJS', // Tajikistan Somoni SRD = "SRD", // 'SURINAMESE DOLLAR'
TMT = 'TMT', // Turkmenistan Manat STD = "STD", // 'SÃO TOMÉ AND PRÍNCIPE DOBRA (PRE-2018)'
TND = 'TND', // Tunisia Dinar SVC = "SVC", // 'SALVADORAN COLÓN'
TOP = 'TOP', // Tonga Pa'anga SYP = "SYP", // 'SYRIAN POUND'
TRY = 'TRY', // Turkey Lira SZL = "SZL", // 'SWAZI LILANGENI'
TTD = 'TTD', // Trinidad and Tobago Dollar THB = "THB", // 'THAI BAHT'
TVD = 'TVD', // Tuvalu Dollar TJS = "TJS", // 'TAJIKISTANI SOMONI'
TWD = 'TWD', // Taiwan New Dollar TMT = "TMT", // 'TURKMENISTANI MANAT'
TZS = 'TZS', // Tanzania Shilling TND = "TND", // 'TUNISIAN DINAR'
UAH = 'UAH', // Ukraine Hryvnia TOP = "TOP", // "TONGAN PA'ANGA"
UGX = 'UGX', // Uganda Shilling TRY = "TRY", // 'TURKISH LIRA'
USD = 'USD', // United States Dollar TTD = "TTD", // 'TRINIDAD & TOBAGO DOLLAR'
UYU = 'UYU', // Uruguay Peso TWD = "TWD", // 'NEW TAIWAN DOLLAR'
UZS = 'UZS', // Uzbekistan Som TZS = "TZS", // 'TANZANIAN SHILLING'
VEF = 'VEF', // Venezuela Bolivar UAH = "UAH", // 'UKRAINIAN HRYVNIA'
VND = 'VND', // Viet Nam Dong UGX = "UGX", // 'UGANDAN SHILLING'
VUV = 'VUV', // Vanuatu Vatu USD = "USD", // 'UNITED STATES DOLLAR'
WST = 'WST', // Samoa Tala UYU = "UYU", // 'URUGUAYAN PESO'
XAF = 'XAF', // Communauté Financière Africaine (BEAC) CFA Franc BEAC UZS = "UZS", // 'UZBEKISTANI SOM'
XCD = 'XCD', // East Caribbean Dollar VND = "VND", // 'VIETNAMESE DONG'
XDR = 'XDR', // International Monetary Fund (IMF) Special Drawing Rights VUV = "VUV", // 'VANUATU VATU'
XOF = 'XOF', // Communauté Financière Africaine (BCEAO) Franc WST = "WST", // 'SAMOAN TALA'
XPF = 'XPF', // Comptoirs Français du Pacifique (CFP) Franc XAF = "XAF", // 'CENTRAL AFRICAN CFA FRANC'
YER = 'YER', // Yemen Rial XCD = "XCD", // 'EAST CARIBBEAN DOLLAR'
ZAR = 'ZAR', // South Africa Rand XOF = "XOF", // 'WEST AFRICAN CFA FRANC'
ZMW = 'ZMW', // Zambia Kwacha XPF = "XPF", // 'CFP FRANC'
ZWD = 'ZWD', // Zimbabwe Dollar YER = "YER", // 'YEMENI RIAL'
ZAR = "ZAR", // 'SOUTH AFRICAN RAND'
ZMW = "ZMW", // 'ZAMBIAN KWACHA'
ZWL = "ZWL", // 'ZIMBABWEAN DOLLAR'
} }
export const CURRENCY_OPTIONS = Object.entries(Currency).map( export const CURRENCY_OPTIONS = Object.entries(Currency).map(

@ -0,0 +1,49 @@
// API from https://github.com/fawazahmed0/currency-api#readme
export const convert = async (
value: number,
fromCurrency: string,
toCurrency: string,
) => {
fromCurrency = fromCurrency.trim().toLowerCase();
toCurrency = toCurrency.trim().toLowerCase();
const url = [
'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies',
fromCurrency,
toCurrency,
].join('/');
return await fetch(url + '.json')
.then((res) => res.json())
.then((data) => value * data[toCurrency]);
};
// https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@{apiVersion}/{date}/{endpoint}
export const convertWithDate = async (
value: number,
fromCurrency: string,
toCurrency: string,
date: Date,
) => {
if (new Date().toDateString === date.toDateString) {
return await convert(value, fromCurrency, toCurrency);
}
fromCurrency = fromCurrency.trim().toLowerCase();
toCurrency = toCurrency.trim().toLowerCase();
// Format date to YYYY-MM-DD
const formattedDate = date.toJSON().substring(0, 10);
const url = [
'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1',
formattedDate,
'currencies',
fromCurrency,
toCurrency,
].join('/');
return await fetch(url + '.json')
.then((res) => res.json())
.then((data) => value * data[toCurrency]);
};

@ -1,5 +1,9 @@
import type { Money } from '~/components/offers/types'; import type { Money } from '~/components/offers/types';
import { Currency } from './CurrencyEnum';
export const baseCurrencyString = Currency.USD.toString();
export function convertMoneyToString({ currency, value }: Money) { export function convertMoneyToString({ currency, value }: Money) {
if (!value) { if (!value) {
return '-'; return '-';

@ -0,0 +1,8 @@
export const createValidationRegex = (
keywordArray: Array<string>,
prepend: string | null | undefined,
) => {
const sortingKeysRegex = keywordArray.join('|');
prepend = prepend != null ? prepend : '';
return new RegExp('^' + prepend + '(' + sortingKeysRegex + ')$');
};

@ -0,0 +1,108 @@
import { Fragment, useEffect, useRef } from 'react';
import { Transition } from '@headlessui/react';
import { CheckIcon } from '@heroicons/react/24/outline';
import { XMarkIcon } from '@heroicons/react/24/solid';
type ToastVariant = 'failure' | 'success';
export type ToastMessage = {
duration?: number;
subtitle?: string;
title: string;
variant: ToastVariant;
};
type Props = Readonly<{
duration?: number;
onClose: () => void;
subtitle?: string;
title: string;
variant: ToastVariant;
}>;
const DEFAULT_DURATION = 5000;
function ToastIcon({ variant }: Readonly<{ variant: ToastVariant }>) {
switch (variant) {
case 'success':
return (
<CheckIcon aria-hidden="true" className="text-success-500 h-6 w-6" />
);
case 'failure':
return (
<XMarkIcon aria-hidden="true" className="text-error-500 h-6 w-6" />
);
}
}
export default function Toast({
duration = DEFAULT_DURATION,
title,
subtitle,
variant,
onClose,
}: Props) {
const timer = useRef<number | null>(null);
function clearTimer() {
if (timer.current == null) {
return;
}
window.clearTimeout(timer.current);
timer.current = null;
}
function close() {
onClose();
clearTimer();
}
useEffect(() => {
timer.current = window.setTimeout(() => {
close();
}, duration);
return () => {
clearTimer();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<Transition
as={Fragment}
enter="transform ease-out duration-300 transition"
enterFrom="translate-y-2 opacity-0 sm:translate-y-2 sm:translate-x-2"
enterTo="translate-y-0 opacity-100 sm:translate-x-0"
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
show={true}>
<div className="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5">
<div className="p-4">
<div className="flex items-start">
<div className="flex-shrink-0">
<ToastIcon variant={variant} />
</div>
<div className="ml-3 w-0 flex-1 space-y-1 pt-0.5">
<p className="text-sm font-medium text-slate-900">{title}</p>
{subtitle && (
<p className="mt-1 text-sm text-slate-500">{subtitle}</p>
)}
</div>
<div className="ml-4 flex flex-shrink-0">
<button
className="focus:ring-brand-500 inline-flex rounded-md bg-white text-slate-400 hover:text-slate-500 focus:outline-none focus:ring-2 focus:ring-offset-2"
type="button"
onClick={close}>
<span className="sr-only">Close</span>
<XMarkIcon aria-hidden="true" className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
</Transition>
);
}

@ -0,0 +1,72 @@
import React, { createContext, useContext, useState } from 'react';
import type { ToastMessage } from './Toast';
import Toast from './Toast';
type Context = Readonly<{
showToast: (message: ToastMessage) => void;
}>;
export const ToastContext = createContext<Context>({
// eslint-disable-next-line @typescript-eslint/no-empty-function
showToast: (_: ToastMessage) => {},
});
const getID = (() => {
let id = 0;
return () => {
return id++;
};
})();
type ToastData = ToastMessage & {
id: number;
};
type Props = Readonly<{
children: React.ReactNode;
}>;
export function useToast() {
return useContext(ToastContext);
}
export default function ToastsProvider({ children }: Props) {
const [toasts, setToasts] = useState<Array<ToastData>>([]);
function showToast({ title, subtitle, variant }: ToastMessage) {
setToasts([{ id: getID(), subtitle, title, variant }, ...toasts]);
}
function closeToast(id: number) {
setToasts((oldToasts) => {
const newToasts = oldToasts.filter((toast) => toast.id !== id);
return newToasts;
});
}
return (
<ToastContext.Provider value={{ showToast }}>
{children}
<div
aria-live="assertive"
className="pointer-events-none fixed inset-0 z-10 flex items-end px-4 py-6 sm:p-6">
<div className="flex w-full flex-col items-center space-y-4 sm:items-end">
{/* Notification panel, dynamically insert this into the live region when it needs to be displayed */}
{toasts.map(({ id, title, subtitle, variant }) => (
<Toast
key={id}
subtitle={subtitle}
title={title}
variant={variant}
onClose={() => {
closeToast(id);
}}
/>
))}
</div>
</div>
</ToastContext.Provider>
);
}

@ -49,6 +49,12 @@ export { default as TextArea } from './TextArea/TextArea';
// TextInput // TextInput
export * from './TextInput/TextInput'; export * from './TextInput/TextInput';
export { default as TextInput } from './TextInput/TextInput'; export { default as TextInput } from './TextInput/TextInput';
// Toast
export * from './Toast/Toast';
export { default as Toast } from './Toast/Toast';
// ToastsProvider
export * from './Toast/ToastsProvider';
export { default as ToastsProvider } from './Toast/ToastsProvider';
// Typeahead // Typeahead
export * from './Typeahead/Typeahead'; export * from './Typeahead/Typeahead';
export { default as Typeahead } from './Typeahead/Typeahead'; export { default as Typeahead } from './Typeahead/Typeahead';

@ -4607,7 +4607,7 @@ atob@^2.1.2:
attr-accept@^2.2.2: attr-accept@^2.2.2:
version "2.2.2" version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b" resolved "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg== integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
autoprefixer@^10.3.7, autoprefixer@^10.4.12, autoprefixer@^10.4.7: autoprefixer@^10.3.7, autoprefixer@^10.4.12, autoprefixer@^10.4.7:
@ -7740,7 +7740,7 @@ file-loader@^6.0.0, file-loader@^6.2.0:
file-selector@^0.6.0: file-selector@^0.6.0:
version "0.6.0" version "0.6.0"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" resolved "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz"
integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
@ -12176,7 +12176,7 @@ react-dom@18.2.0, react-dom@^18.2.0:
react-dropzone@^14.2.3: react-dropzone@^14.2.3:
version "14.2.3" version "14.2.3"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" resolved "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz"
integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==
dependencies: dependencies:
attr-accept "^2.2.2" attr-accept "^2.2.2"
@ -14163,47 +14163,47 @@ tty-browserify@0.0.0:
resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz" resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz"
integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw== integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==
turbo-darwin-64@1.5.5: turbo-darwin-64@1.5.6:
version "1.5.5" version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.5.5.tgz#710d4e7999066bd4f500456f7cd1c30f6e6205ed" resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.5.6.tgz#2e0e14343c84dde33b5a09ea5389ee6a9565779c"
integrity sha512-HvEn6P2B+NXDekq9LRpRgUjcT9/oygLTcK47U0qsAJZXRBSq/2hvD7lx4nAwgY/4W3rhYJeWtHTzbhoN6BXqGQ== integrity sha512-CWdXMwenBS2+QXIR2Czx7JPnAcoMzWx/QwTDcHVxZyeayMHgz8Oq5AHCtfaHDSfV8YhD3xa0GLSk6+cFt+W8BQ==
turbo-darwin-arm64@1.5.5: turbo-darwin-arm64@1.5.6:
version "1.5.5" version "1.5.6"
resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.5.5.tgz" resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.5.6.tgz"
integrity sha512-Dmxr09IUy6M0nc7/xWod9galIO2DD500B75sJSkHeT+CCdJOWnlinux0ZPF8CSygNqymwYO8AO2l15/6yxcycg== integrity sha512-c/aXgW9JuXT2bJSKf01pdSDQKnrdcdj3WFKmKiVldb9We6eqFzI0fLHBK97k5LM/OesmRMfCMQ2Cv2DU8RqBAA==
turbo-linux-64@1.5.5: turbo-linux-64@1.5.6:
version "1.5.5" version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.5.5.tgz#f31eb117a9b605f5731048c50473bff903850047" resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.5.6.tgz#e7ddaf7a87084dfdd9c6d79efb41084d75439b31"
integrity sha512-wd07TZ4zXXWjzZE00FcFMLmkybQQK/NV9ff66vvAV0vdiuacSMBCNLrD6Mm4ncfrUPW/rwFW5kU/7hyuEqqtDw== integrity sha512-y/jNF7SG+XJEwk2GxIqy3g4dj/a0PgZKDGyOkp24qp4KBRcHBl6dI1ZEfNed30EhEqmW4F5Dr7IpeCZoqgbrMg==
turbo-linux-arm64@1.5.5: turbo-linux-arm64@1.5.6:
version "1.5.5" version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.5.5.tgz#b9ce6912ae6477e829355d6f012500bfef58669d" resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.5.6.tgz#6445f00f84e0f356a6a369ba2d75ede43aaeb796"
integrity sha512-q3q33tuo74R7gicnfvFbnZZvqmlq7Vakcvx0eshifnJw4PR+oMnTCb4w8ElVFx070zsb8DVTibq99y8NJH8T1Q== integrity sha512-FRcxPtW7eFrbR3QaYBVX8cK7i+2Cerqi6F0t5ulcq+d1OGSdSW3l35rPPyJdwCzCy+k/S9sBcyCV0RtbS6RKCQ==
turbo-windows-64@1.5.5: turbo-windows-64@1.5.6:
version "1.5.5" version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.5.5.tgz#609098de3bc6178f733615d21b06d5c1602637eb" resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.5.6.tgz#3638d5297319157031e4dc906dbae53a1db8562c"
integrity sha512-lPp9kHonNFfqgovbaW+UAPO5cLmoAN+m3G3FzqcrRPnlzt97vXYsDhDd/4Zy3oAKoAcprtP4CGy0ddisqsKTVw== integrity sha512-/5KIExY7zbrbeL5fhKGuO85u5VtJ3Ue4kI0MbYCNnTGe7a10yTYkwswgtGihsgEF4AW0Nm0159aHmXZS2Le8IA==
turbo-windows-arm64@1.5.5: turbo-windows-arm64@1.5.6:
version "1.5.5" version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.5.5.tgz#60522e1e347a54c64bdddb68089fc322ee19c3d7" resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.5.6.tgz#9eff9d13721be0b905b0aad07667507380f738fe"
integrity sha512-3AfGULKNZiZVrEzsIE+W79ZRW1+f5r4nM4wLlJ1PTBHyRxBZdD6KTH1tijGfy/uTlcV5acYnKHEkDc6Q9PAXGQ== integrity sha512-p+LQN9O39+rZuOAyc6BzyVGvdEKo+v+XmtdeyZsZpfj4xuOLtsEptW1w6cUD439u0YcPknuccGq1MQ0lXQ6Xuw==
turbo@latest: turbo@latest:
version "1.5.5" version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.5.5.tgz#9fc3a917c914ffa113c260a4eadb4bc632eee227" resolved "https://registry.npmjs.org/turbo/-/turbo-1.5.6.tgz"
integrity sha512-PVQSDl0STC9WXIyHcYUWs9gXsf8JjQig/FuHfuB8N6+XlgCGB3mPbfMEE6zrChGz2hufH4/guKRX1XJuNL6XTA== integrity sha512-xJO/fhiMo4lI62iGR9OgUfJTC9tnnuoMwNC52IfvvBDEPlA8RWGMS8SFpDVG9bNCXvVRrtUTNJXMe6pJWBiOTA==
optionalDependencies: optionalDependencies:
turbo-darwin-64 "1.5.5" turbo-darwin-64 "1.5.6"
turbo-darwin-arm64 "1.5.5" turbo-darwin-arm64 "1.5.6"
turbo-linux-64 "1.5.5" turbo-linux-64 "1.5.6"
turbo-linux-arm64 "1.5.5" turbo-linux-arm64 "1.5.6"
turbo-windows-64 "1.5.5" turbo-windows-64 "1.5.6"
turbo-windows-arm64 "1.5.5" turbo-windows-arm64 "1.5.6"
type-check@^0.4.0, type-check@~0.4.0: type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0" version "0.4.0"
@ -14641,7 +14641,7 @@ uuid-browser@^3.1.0:
uuid@^3.3.2: uuid@^3.3.2:
version "3.4.0" version "3.4.0"
resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
uuid@^8.3.2: uuid@^8.3.2:

Loading…
Cancel
Save