Merge branch 'main' into questions/optimize-queries

pull/411/head
Jeff Sieu 3 years ago
commit 7a7bec2165

@ -0,0 +1,5 @@
-- AlterTable
ALTER TABLE "ResumesComment" ADD COLUMN "parentId" TEXT;
-- AddForeignKey
ALTER TABLE "ResumesComment" ADD CONSTRAINT "ResumesComment_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "ResumesComment"("id") ON DELETE SET NULL ON UPDATE CASCADE;

@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `upvotes` to the `QuestionsQuestion` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "QuestionsQuestion" ADD COLUMN "lastSeenAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "upvotes" INTEGER NOT NULL;
-- AlterTable
ALTER TABLE "QuestionsQuestionEncounter" ADD COLUMN "netVotes" INTEGER NOT NULL DEFAULT 0;

@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the column `netVotes` on the `QuestionsQuestionEncounter` table. All the data in the column will be lost.
*/
-- AlterTable
ALTER TABLE "QuestionsQuestion" ALTER COLUMN "lastSeenAt" DROP DEFAULT,
ALTER COLUMN "upvotes" SET DEFAULT 0;
-- AlterTable
ALTER TABLE "QuestionsQuestionEncounter" DROP COLUMN "netVotes";

@ -0,0 +1,5 @@
-- CreateIndex
CREATE INDEX "QuestionsQuestion_lastSeenAt_id_idx" ON "QuestionsQuestion"("lastSeenAt", "id");
-- CreateIndex
CREATE INDEX "QuestionsQuestion_upvotes_id_idx" ON "QuestionsQuestion"("upvotes", "id");

@ -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;

@ -140,6 +140,7 @@ model ResumesComment {
id String @id @default(cuid())
userId String
resumeId String
parentId String?
description String @db.Text
section ResumesSection
createdAt DateTime @default(now())
@ -147,6 +148,8 @@ model ResumesComment {
resume ResumesResume @relation(fields: [resumeId], references: [id], onDelete: Cascade)
votes ResumesCommentVote[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
parent ResumesComment? @relation("parentComment", fields: [parentId], references: [id])
children ResumesComment[] @relation("parentComment")
}
enum ResumesSection {
@ -202,9 +205,9 @@ model OffersBackground {
totalYoe Int
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)
offersProfileId String @unique
@ -249,9 +252,15 @@ model OffersExperience {
model OffersCurrency {
id String @id @default(cuid())
value Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
value Float
currency String
baseValue Float
baseCurrency String @default("USD")
// Experience
OffersExperienceTotalCompensation OffersExperience? @relation("ExperienceTotalCompensation")
OffersExperienceMonthlySalary OffersExperience? @relation("ExperienceMonthlySalary")
@ -395,6 +404,8 @@ model QuestionsQuestion {
userId String?
content String @db.Text
questionType QuestionsQuestionType
lastSeenAt DateTime
upvotes Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -403,6 +414,9 @@ model QuestionsQuestion {
votes QuestionsQuestionVote[]
comments QuestionsQuestionComment[]
answers QuestionsAnswer[]
@@index([lastSeenAt, id])
@@index([upvotes, id])
}
model QuestionsQuestionEncounter {

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

@ -100,7 +100,8 @@ export default function FilterSection<
{isSingleSelect ? (
<div className="px-1.5">
<RadioList
label=""
isLabelHidden={true}
label={label}
value={options.find((option) => option.checked)?.value}
onChange={(value) => {
onOptionChange(value);

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

@ -1,18 +1,11 @@
import clsx from 'clsx';
import type { Dispatch, SetStateAction } from 'react';
import { useState } from 'react';
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from '@heroicons/react/20/solid';
import { ChevronUpIcon } from '@heroicons/react/20/solid';
import { FaceSmileIcon } from '@heroicons/react/24/outline';
import { Vote } from '@prisma/client';
import { Button, TextArea } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import ResumeCommentEditForm from './comment/ResumeCommentEditForm';
import ResumeCommentReplyForm from './comment/ResumeCommentReplyForm';
import ResumeCommentVoteButtons from './comment/ResumeCommentVoteButtons';
import ResumeUserBadges from '../badges/ResumeUserBadges';
import ResumeExpandableText from '../shared/ResumeExpandableText';
@ -23,141 +16,55 @@ type ResumeCommentListItemProps = {
userId: string | undefined;
};
type ICommentInput = {
description: string;
};
export default function ResumeCommentListItem({
comment,
userId,
}: ResumeCommentListItemProps) {
const isCommentOwner = userId === comment.user.userId;
const [isEditingComment, setIsEditingComment] = useState(false);
const [upvoteAnimation, setUpvoteAnimation] = useState(false);
const [downvoteAnimation, setDownvoteAnimation] = useState(false);
const {
register,
handleSubmit,
setValue,
formState: { errors, isDirty },
reset,
} = useForm<ICommentInput>({
defaultValues: {
description: comment.description,
},
});
const trpcContext = trpc.useContext();
const commentUpdateMutation = trpc.useMutation(
'resumes.comments.user.update',
{
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.list']);
},
},
);
// COMMENT VOTES
const commentVotesQuery = trpc.useQuery([
'resumes.comments.votes.list',
{ commentId: comment.id },
]);
const commentVotesUpsertMutation = trpc.useMutation(
'resumes.comments.votes.user.upsert',
{
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
},
},
);
const commentVotesDeleteMutation = trpc.useMutation(
'resumes.comments.votes.user.delete',
{
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
},
},
);
// FORM ACTIONS
const onCancel = () => {
reset({ description: comment.description });
setIsEditingComment(false);
};
const onSubmit: SubmitHandler<ICommentInput> = async (data) => {
const { id } = comment;
return commentUpdateMutation.mutate(
{
id,
...data,
},
{
onSuccess: () => {
setIsEditingComment(false);
},
},
);
};
const setFormValue = (value: string) => {
setValue('description', value.trim(), { shouldDirty: true });
};
const onVote = async (
value: Vote,
setAnimation: Dispatch<SetStateAction<boolean>>,
) => {
setAnimation(true);
if (commentVotesQuery.data?.userVote?.value === value) {
return commentVotesDeleteMutation.mutate(
{
commentId: comment.id,
},
{
onSettled: async () => setAnimation(false),
},
);
}
return commentVotesUpsertMutation.mutate(
{
commentId: comment.id,
value,
},
{
onSettled: async () => setAnimation(false),
},
);
};
const [isReplyingComment, setIsReplyingComment] = useState(false);
const [showReplies, setShowReplies] = useState(true);
return (
<div className="border-primary-300 w-11/12 min-w-fit rounded-md border-2 bg-white p-2 drop-shadow-md">
<div
className={clsx(
'min-w-fit rounded-md bg-white ',
!comment.parentId &&
'w-11/12 border-2 border-indigo-300 p-2 drop-shadow-md',
)}>
<div className="flex flex-row space-x-2 p-1 align-top">
{/* Image Icon */}
{comment.user.image ? (
<img
alt={comment.user.name ?? 'Reviewer'}
className="mt-1 h-8 w-8 rounded-full"
className={clsx(
'mt-1 rounded-full',
comment.parentId ? 'h-6 w-6' : 'h-8 w-8 ',
)}
src={comment.user.image!}
/>
) : (
<FaceSmileIcon className="h-8 w-8 rounded-full" />
<FaceSmileIcon
className={clsx(
'mt-1 rounded-full',
comment.parentId ? 'h-6 w-6' : 'h-8 w-8 ',
)}
/>
)}
<div className="flex w-full flex-col space-y-1">
{/* Name and creation time */}
<div className="flex flex-row justify-between">
<div className="flex flex-row items-center space-x-1">
<p className="font-medium">
<p
className={clsx(
'font-medium text-black',
!!comment.parentId && 'text-sm',
)}>
{comment.user.name ?? 'Reviewer ABC'}
</p>
<p className="text-primary-800 text-xs font-medium">
<p className="text-xs font-medium text-indigo-800">
{isCommentOwner ? '(Me)' : ''}
</p>
@ -174,112 +81,92 @@ export default function ResumeCommentListItem({
{/* Description */}
{isEditingComment ? (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex-column mt-1 space-y-2">
<TextArea
{...(register('description', {
required: 'Comments cannot be empty!',
}),
{})}
defaultValue={comment.description}
disabled={commentUpdateMutation.isLoading}
errorMessage={errors.description?.message}
label=""
placeholder="Leave your comment here"
onChange={setFormValue}
/>
<div className="flex-row space-x-2">
<Button
disabled={commentUpdateMutation.isLoading}
label="Cancel"
size="sm"
variant="tertiary"
onClick={onCancel}
/>
<Button
disabled={!isDirty || commentUpdateMutation.isLoading}
isLoading={commentUpdateMutation.isLoading}
label="Confirm"
size="sm"
type="submit"
variant="primary"
<ResumeCommentEditForm
comment={comment}
setIsEditingComment={setIsEditingComment}
/>
</div>
</div>
</form>
) : (
<ResumeExpandableText text={comment.description} />
<ResumeExpandableText
key={comment.description}
text={comment.description}
/>
)}
{/* Upvote and edit */}
<div className="flex flex-row space-x-1 pt-1 align-middle">
<ResumeCommentVoteButtons commentId={comment.id} userId={userId} />
{/* Action buttons; only present for authenticated user when not editing/replying */}
{userId && !isEditingComment && !isReplyingComment && (
<>
{isCommentOwner && (
<button
disabled={
!userId ||
commentVotesQuery.isLoading ||
commentVotesUpsertMutation.isLoading ||
commentVotesDeleteMutation.isLoading
}
className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
type="button"
onClick={() => onVote(Vote.UPVOTE, setUpvoteAnimation)}>
<ArrowUpCircleIcon
className={clsx(
'h-4 w-4',
commentVotesQuery.data?.userVote?.value === Vote.UPVOTE ||
upvoteAnimation
? 'fill-indigo-500'
: 'fill-gray-400',
userId &&
!downvoteAnimation &&
!upvoteAnimation &&
'hover:fill-indigo-500',
upvoteAnimation &&
'animate-[bounce_0.5s_infinite] cursor-default',
)}
/>
onClick={() => setIsEditingComment(true)}>
Edit
</button>
)}
<div className="text-xs">
{commentVotesQuery.data?.numVotes ?? 0}
{!comment.parentId && (
<button
className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
type="button"
onClick={() => setIsReplyingComment(true)}>
Reply
</button>
)}
</>
)}
</div>
{/* Reply Form */}
{isReplyingComment && (
<ResumeCommentReplyForm
parentId={comment.id}
resumeId={comment.resumeId}
section={comment.section}
setIsReplyingComment={setIsReplyingComment}
/>
)}
{/* Replies */}
{comment.children.length > 0 && (
<div className="min-w-fit space-y-1 pt-2">
<button
disabled={
!userId ||
commentVotesQuery.isLoading ||
commentVotesUpsertMutation.isLoading ||
commentVotesDeleteMutation.isLoading
}
className="flex items-center space-x-1 rounded-md text-xs font-medium text-indigo-800 hover:text-indigo-300"
type="button"
onClick={() => onVote(Vote.DOWNVOTE, setDownvoteAnimation)}>
<ArrowDownCircleIcon
onClick={() => setShowReplies(!showReplies)}>
<ChevronUpIcon
className={clsx(
'h-4 w-4',
commentVotesQuery.data?.userVote?.value === Vote.DOWNVOTE ||
downvoteAnimation
? 'fill-red-500'
: 'fill-gray-400',
userId &&
!downvoteAnimation &&
!upvoteAnimation &&
'hover:fill-red-500',
downvoteAnimation &&
'animate-[bounce_0.5s_infinite] cursor-default',
'h-5 w-5 ',
!showReplies && 'rotate-180 transform',
)}
/>
<span>{showReplies ? 'Hide replies' : 'Show replies'}</span>
</button>
{isCommentOwner && !isEditingComment && (
<button
className="text-primary-800 hover:text-primary-400 px-1 text-xs"
type="button"
onClick={() => setIsEditingComment(true)}>
Edit
</button>
{showReplies && (
<div className="flex flex-row">
<div className="relative flex flex-col px-2 py-2">
<div className="flex-grow border-r border-gray-300" />
</div>
<div className="flex flex-col space-y-1">
{comment.children.map((child) => {
return (
<ResumeCommentListItem
key={child.id}
comment={child}
userId={userId}
/>
);
})}
</div>
</div>
)}
</div>
)}
</div>
</div>
</div>

@ -0,0 +1,106 @@
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import { Button, TextArea } from '@tih/ui';
import { trpc } from '~/utils/trpc';
import type { ResumeComment } from '~/types/resume-comments';
type ResumeCommentEditFormProps = {
comment: ResumeComment;
setIsEditingComment: (value: boolean) => void;
};
type ICommentInput = {
description: string;
};
export default function ResumeCommentEditForm({
comment,
setIsEditingComment,
}: ResumeCommentEditFormProps) {
const {
register,
handleSubmit,
setValue,
formState: { errors, isDirty },
reset,
} = useForm<ICommentInput>({
defaultValues: {
description: comment.description,
},
});
const trpcContext = trpc.useContext();
const commentUpdateMutation = trpc.useMutation(
'resumes.comments.user.update',
{
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.list']);
},
},
);
const onCancel = () => {
reset({ description: comment.description });
setIsEditingComment(false);
};
const onSubmit: SubmitHandler<ICommentInput> = async (data) => {
const { id } = comment;
return commentUpdateMutation.mutate(
{
id,
...data,
},
{
onSuccess: () => {
setIsEditingComment(false);
},
},
);
};
const setFormValue = (value: string) => {
setValue('description', value.trim(), { shouldDirty: true });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex-column mt-1 space-y-2">
<TextArea
{...(register('description', {
required: 'Comments cannot be empty!',
}),
{})}
defaultValue={comment.description}
disabled={commentUpdateMutation.isLoading}
errorMessage={errors.description?.message}
label=""
placeholder="Leave your comment here"
onChange={setFormValue}
/>
<div className="flex-row space-x-2">
<Button
disabled={commentUpdateMutation.isLoading}
label="Cancel"
size="sm"
variant="tertiary"
onClick={onCancel}
/>
<Button
disabled={!isDirty || commentUpdateMutation.isLoading}
isLoading={commentUpdateMutation.isLoading}
label="Confirm"
size="sm"
type="submit"
variant="primary"
/>
</div>
</div>
</form>
);
}

@ -0,0 +1,107 @@
import type { SubmitHandler } from 'react-hook-form';
import { useForm } from 'react-hook-form';
import type { ResumesSection } from '@prisma/client';
import { Button, TextArea } from '@tih/ui';
import { trpc } from '~/utils/trpc';
type ResumeCommentEditFormProps = {
parentId: string;
resumeId: string;
section: ResumesSection;
setIsReplyingComment: (value: boolean) => void;
};
type IReplyInput = {
description: string;
};
export default function ResumeCommentReplyForm({
parentId,
setIsReplyingComment,
resumeId,
section,
}: ResumeCommentEditFormProps) {
const {
register,
handleSubmit,
setValue,
formState: { errors, isDirty },
reset,
} = useForm<IReplyInput>({
defaultValues: {
description: '',
},
});
const trpcContext = trpc.useContext();
const commentReplyMutation = trpc.useMutation('resumes.comments.user.reply', {
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.list']);
},
});
const onCancel = () => {
reset({ description: '' });
setIsReplyingComment(false);
};
const onSubmit: SubmitHandler<IReplyInput> = async (data) => {
return commentReplyMutation.mutate(
{
parentId,
resumeId,
section,
...data,
},
{
onSuccess: () => {
setIsReplyingComment(false);
},
},
);
};
const setFormValue = (value: string) => {
setValue('description', value.trim(), { shouldDirty: true });
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="flex-column space-y-2 pt-2">
<TextArea
{...(register('description', {
required: 'Reply cannot be empty!',
}),
{})}
defaultValue=""
disabled={commentReplyMutation.isLoading}
errorMessage={errors.description?.message}
label=""
placeholder="Leave your reply here"
onChange={setFormValue}
/>
<div className="flex-row space-x-2">
<Button
disabled={commentReplyMutation.isLoading}
label="Cancel"
size="sm"
variant="tertiary"
onClick={onCancel}
/>
<Button
disabled={!isDirty || commentReplyMutation.isLoading}
isLoading={commentReplyMutation.isLoading}
label="Confirm"
size="sm"
type="submit"
variant="primary"
/>
</div>
</div>
</form>
);
}

@ -0,0 +1,131 @@
import clsx from 'clsx';
import { useState } from 'react';
import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from '@heroicons/react/20/solid';
import { Vote } from '@prisma/client';
import { trpc } from '~/utils/trpc';
type ResumeCommentVoteButtonsProps = {
commentId: string;
userId: string | undefined;
};
export default function ResumeCommentVoteButtons({
commentId,
userId,
}: ResumeCommentVoteButtonsProps) {
const [upvoteAnimation, setUpvoteAnimation] = useState(false);
const [downvoteAnimation, setDownvoteAnimation] = useState(false);
const trpcContext = trpc.useContext();
// COMMENT VOTES
const commentVotesQuery = trpc.useQuery([
'resumes.comments.votes.list',
{ commentId },
]);
const commentVotesUpsertMutation = trpc.useMutation(
'resumes.comments.votes.user.upsert',
{
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
},
},
);
const commentVotesDeleteMutation = trpc.useMutation(
'resumes.comments.votes.user.delete',
{
onSuccess: () => {
// Comment updated, invalidate query to trigger refetch
trpcContext.invalidateQueries(['resumes.comments.votes.list']);
},
},
);
const onVote = async (value: Vote, setAnimation: (_: boolean) => void) => {
setAnimation(true);
if (commentVotesQuery.data?.userVote?.value === value) {
return commentVotesDeleteMutation.mutate(
{
commentId,
},
{
onSettled: async () => setAnimation(false),
},
);
}
return commentVotesUpsertMutation.mutate(
{
commentId,
value,
},
{
onSettled: async () => setAnimation(false),
},
);
};
return (
<>
<button
disabled={
!userId ||
commentVotesQuery.isLoading ||
commentVotesUpsertMutation.isLoading ||
commentVotesDeleteMutation.isLoading
}
type="button"
onClick={() => onVote(Vote.UPVOTE, setUpvoteAnimation)}>
<ArrowUpCircleIcon
className={clsx(
'h-4 w-4',
commentVotesQuery.data?.userVote?.value === Vote.UPVOTE ||
upvoteAnimation
? 'fill-indigo-500'
: 'fill-gray-400',
userId &&
!downvoteAnimation &&
!upvoteAnimation &&
'hover:fill-indigo-500',
upvoteAnimation && 'animate-[bounce_0.5s_infinite] cursor-default',
)}
/>
</button>
<div className="flex min-w-[1rem] justify-center text-xs">
{commentVotesQuery.data?.numVotes ?? 0}
</div>
<button
disabled={
!userId ||
commentVotesQuery.isLoading ||
commentVotesUpsertMutation.isLoading ||
commentVotesDeleteMutation.isLoading
}
type="button"
onClick={() => onVote(Vote.DOWNVOTE, setDownvoteAnimation)}>
<ArrowDownCircleIcon
className={clsx(
'h-4 w-4',
commentVotesQuery.data?.userVote?.value === Vote.DOWNVOTE ||
downvoteAnimation
? 'fill-red-500'
: 'fill-gray-400',
userId &&
!downvoteAnimation &&
!upvoteAnimation &&
'hover:fill-red-500',
downvoteAnimation &&
'animate-[bounce_0.5s_infinite] cursor-default',
)}
/>
</button>
</>
);
}

@ -1,5 +1,5 @@
import clsx from 'clsx';
import { useEffect, useState } from 'react';
import { useLayoutEffect, useRef, useState } from 'react';
type ResumeExpandableTextProps = Readonly<{
text: string;
@ -8,17 +8,17 @@ type ResumeExpandableTextProps = Readonly<{
export default function ResumeExpandableText({
text,
}: ResumeExpandableTextProps) {
const ref = useRef<HTMLSpanElement>(null);
const [isExpanded, setIsExpanded] = useState(false);
const [descriptionOverflow, setDescriptionOverflow] = useState(false);
useEffect(() => {
const lines = text.split(/\r\n|\r|\n/);
if (lines.length > 3) {
useLayoutEffect(() => {
if (ref.current && ref.current.clientHeight < ref.current.scrollHeight) {
setDescriptionOverflow(true);
} else {
setDescriptionOverflow(false);
}
}, [text]);
}, [ref]);
const onSeeActionClicked = () => {
setIsExpanded((prevExpanded) => !prevExpanded);
@ -27,6 +27,7 @@ export default function ResumeExpandableText({
return (
<div>
<span
ref={ref}
className={clsx(
'line-clamp-3 whitespace-pre-wrap text-sm',
isExpanded ? 'line-clamp-none' : '',

@ -1,12 +1,14 @@
import clsx from 'clsx';
import { signIn } from 'next-auth/react';
type Props = Readonly<{
className?: string;
text: string;
}>;
export default function ResumeSignInButton({ text }: Props) {
export default function ResumeSignInButton({ text, className }: Props) {
return (
<div className="flex justify-center pt-4">
<div className={clsx('flex justify-center pt-4', className)}>
<p>
<a
className="text-primary-800 hover:text-primary-500"

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

@ -40,7 +40,7 @@ function Test() {
deleteCommentMutation.mutate({
id: 'cl97fprun001j7iyg6ev9x983',
profileId: 'cl96stky5002ew32gx2kale2x',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1',
token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
userId: 'cl97dl51k001e7iygd5v5gt58',
});
};
@ -84,7 +84,7 @@ function Test() {
const handleLink = () => {
addToUserProfileMutation.mutate({
profileId: 'cl9efyn9p004ww3u42mjgl1vn',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
userId: 'cl9ehvpng0000w3ec2mpx0bdd',
});
};
@ -103,11 +103,10 @@ function Test() {
],
experiences: [
{
companyId: 'cl9h0bqu50000txxwkhmshhxz',
companyId: 'cl9j4yawz0003utlp1uaa1t8o',
durationInMonths: 24,
jobType: 'FULLTIME',
level: 'Junior',
// "monthlySalary": undefined,
specialization: 'Front End',
title: 'Software Engineer',
totalCompensation: {
@ -132,7 +131,7 @@ function Test() {
{
comments: 'I am a Raffles Institution almumni',
// Comments: '',
companyId: 'cl9h0bqu50000txxwkhmshhxz',
companyId: 'cl9j4yawz0003utlp1uaa1t8o',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
@ -140,28 +139,28 @@ function Test() {
offersFullTime: {
baseSalary: {
currency: 'SGD',
value: 84000,
value: 2222,
},
bonus: {
currency: 'SGD',
value: 20000,
value: 2222,
},
level: 'Junior',
specialization: 'Front End',
stocks: {
currency: 'SGD',
value: 100,
value: 0,
},
title: 'Software Engineer',
totalCompensation: {
currency: 'SGD',
value: 104100,
value: 4444,
},
},
},
{
comments: '',
companyId: 'cl9h0bqu50000txxwkhmshhxz',
companyId: 'cl9j4yawz0003utlp1uaa1t8o',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
@ -192,14 +191,14 @@ function Test() {
});
};
const profileId = 'cl9efyn9p004ww3u42mjgl1vn'; // Remember to change this filed after testing deleting
const profileId = 'cl9j50xzk008vutfqg6mta2ey'; // Remember to change this filed after testing deleting
const data = trpc.useQuery(
[
`offers.profile.listOne`,
{
profileId,
token:
'd14666ff76e267c9e99445844b41410e83874936d0c07e664db73ff0ea76919e',
'24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
},
],
{
@ -223,7 +222,7 @@ function Test() {
const handleDelete = (id: string) => {
deleteMutation.mutate({
profileId: id,
token: 'e7effd2a40adba2deb1ddea4fb9f1e6c3c98ab0a85a88ed1567fc2a107fdb445',
token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
});
};
@ -241,10 +240,10 @@ function Test() {
background: {
educations: [
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
backgroundId: 'cl9i68fv60001tthj23g9tuv4',
endDate: new Date('2018-09-30T07:58:54.000Z'),
field: 'Computer Science',
id: 'cl96stky6002gw32gey2ffawd',
id: 'cl9i87y7z004otthjmpsd48wo',
school: 'National University of Singapore',
startDate: new Date('2014-09-30T07:58:54.000Z'),
type: 'Bachelors',
@ -252,20 +251,20 @@ function Test() {
],
experiences: [
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
backgroundId: 'cl9i68fv60001tthj23g9tuv4',
company: {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
id: 'cl95u79f000007im531ysjg79',
id: 'cl9j4yawz0003utlp1uaa1t8o',
logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta',
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl9h0bqu50000txxwkhmshhxz',
companyId: 'cl9j4yawz0003utlp1uaa1t8o',
durationInMonths: 24,
id: 'cl96stky6002iw32gpt6t87s2',
// Id: 'cl9j4yawz0003utlp1uaa1t8o',
jobType: 'FULLTIME',
level: 'Junior',
monthlySalary: null,
@ -274,57 +273,33 @@ function Test() {
title: 'Software Engineer',
totalCompensation: {
currency: 'SGD',
id: 'cl96stky6002jw32g73svfacr',
value: 104100,
id: 'cl9i68fvc0005tthj7r1rhvb1',
value: 100,
},
totalCompensationId: 'cl96stky6002jw32g73svfacr',
totalCompensationId: 'cl9i68fvc0005tthj7r1rhvb1',
},
],
id: 'cl96stky6002fw32g6vj4meyr',
offersProfileId: 'cl96stky5002ew32gx2kale2x',
id: 'cl9i68fv60001tthj23g9tuv4',
offersProfileId: 'cl9i68fv60000tthj8t3zkox0',
specificYoes: [
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
backgroundId: 'cl9i68fv60001tthj23g9tuv4',
domain: 'Backend',
id: 'cl96t7890004tw32g5in3px5j',
yoe: 2,
id: 'cl9i68fvc0008tthjlxslzfo4',
yoe: 5,
},
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
backgroundId: 'cl9i68fv60001tthj23g9tuv4',
domain: 'Backend',
id: 'cl96tb87x004xw32gnu17jbzv',
yoe: 2,
},
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
domain: 'Backend',
id: 'cl976t39z00007iygt3np3cgo',
yoe: 2,
},
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
domain: 'Front End',
id: 'cl96stky7002mw32gn4jc7uml',
yoe: 2,
},
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
domain: 'Full Stack',
id: 'cl96stky7002nw32gpprghtxr',
yoe: 2,
},
{
backgroundId: 'cl96stky6002fw32g6vj4meyr',
domain: 'Backend',
id: 'cl976we5h000p7iygiomdo9fh',
yoe: 2,
id: 'cl9i68fvc0009tthjwol3285l',
yoe: 4,
},
],
totalYoe: 6,
totalYoe: 1,
},
createdAt: '2022-10-13T08:28:13.518Z',
discussion: [],
id: 'cl96stky5002ew32gx2kale2x',
// Discussion: [],
id: 'cl9i68fv60000tthj8t3zkox0',
isEditable: true,
offers: [
{
@ -333,14 +308,14 @@ function Test() {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
id: 'cl95u79f000007im531ysjg79',
id: 'cl9j4yawz0003utlp1uaa1t8o',
logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta',
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl9h0bqu50000txxwkhmshhxz',
id: 'cl976t4de00047iygl0zbce11',
companyId: 'cl9j4yawz0003utlp1uaa1t8o',
id: 'cl9i68fve000ntthj5h9yvqnh',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
@ -348,253 +323,253 @@ function Test() {
offersFullTime: {
baseSalary: {
currency: 'SGD',
id: 'cl976t4de00067iyg3pjir7k9',
id: 'cl9i68fve000ptthjn55hpoe4',
value: 1999999999,
},
baseSalaryId: 'cl976t4de00067iyg3pjir7k9',
baseSalaryId: 'cl9i68fve000ptthjn55hpoe4',
bonus: {
currency: 'SGD',
id: 'cl976t4de00087iygcnlmh8aw',
id: 'cl9i68fve000rtthjqo2ktljt',
value: 1410065407,
},
bonusId: 'cl976t4de00087iygcnlmh8aw',
id: 'cl976t4de00057iygq3ktce3v',
bonusId: 'cl9i68fve000rtthjqo2ktljt',
id: 'cl9i68fve000otthjqk0g01k0',
level: 'EXPERT',
specialization: 'FRONTEND',
stocks: {
currency: 'SGD',
id: 'cl976t4df000a7iygkrsgr1xh',
id: 'cl9i68fvf000ttthjt2ode0cc',
value: -558038585,
},
stocksId: 'cl976t4df000a7iygkrsgr1xh',
stocksId: 'cl9i68fvf000ttthjt2ode0cc',
title: 'Software Engineer',
totalCompensation: {
currency: 'SGD',
id: 'cl976t4df000c7iyg73ryf5uw',
id: 'cl9i68fvf000vtthjg90s48nj',
value: 55555555,
},
totalCompensationId: 'cl976t4df000c7iyg73ryf5uw',
totalCompensationId: 'cl9i68fvf000vtthjg90s48nj',
},
offersFullTimeId: 'cl976t4de00057iygq3ktce3v',
offersFullTimeId: 'cl9i68fve000otthjqk0g01k0',
offersIntern: null,
offersInternId: null,
profileId: 'cl96stky5002ew32gx2kale2x',
},
{
comments: '',
company: {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
id: 'cl95u79f000007im531ysjg79',
logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta',
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl9h0bqu50000txxwkhmshhxz',
id: 'cl96stky80031w32gau9mu1gs',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
negotiationStrategy: 'Leveraged having million offers',
offersFullTime: {
baseSalary: {
currency: 'SGD',
id: 'cl96stky80033w32gxw5goc4z',
value: 84000,
},
baseSalaryId: 'cl96stky80033w32gxw5goc4z',
bonus: {
currency: 'SGD',
id: 'cl96stky80035w32gajjwdo1p',
value: 123456789,
},
bonusId: 'cl96stky80035w32gajjwdo1p',
id: 'cl96stky80032w32gep9ovgj3',
level: 'Junior',
specialization: 'Front End',
stocks: {
currency: 'SGD',
id: 'cl96stky90037w32gu04t6ybh',
value: 100,
},
stocksId: 'cl96stky90037w32gu04t6ybh',
title: 'Software Engineer',
totalCompensation: {
currency: 'SGD',
id: 'cl96stky90039w32glbpktd0o',
value: 104100,
},
totalCompensationId: 'cl96stky90039w32glbpktd0o',
},
offersFullTimeId: 'cl96stky80032w32gep9ovgj3',
offersIntern: null,
offersInternId: null,
profileId: 'cl96stky5002ew32gx2kale2x',
},
{
comments: '',
company: {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
id: 'cl95u79f000007im531ysjg79',
logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta',
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl9h0bqu50000txxwkhmshhxz',
id: 'cl96stky9003bw32gc3l955vr',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
negotiationStrategy: 'LOst out having multiple offers',
offersFullTime: {
baseSalary: {
currency: 'SGD',
id: 'cl96stky9003dw32gcvqbijlo',
value: 1,
},
baseSalaryId: 'cl96stky9003dw32gcvqbijlo',
bonus: {
currency: 'SGD',
id: 'cl96stky9003fw32goc3zqxwr',
value: 0,
},
bonusId: 'cl96stky9003fw32goc3zqxwr',
id: 'cl96stky9003cw32g5v10izfu',
level: 'Senior',
specialization: 'Front End',
stocks: {
currency: 'SGD',
id: 'cl96stky9003hw32g1lbbkqqr',
value: 999999,
},
stocksId: 'cl96stky9003hw32g1lbbkqqr',
title: 'Software Engineer DOG',
totalCompensation: {
currency: 'SGD',
id: 'cl96stky9003jw32gzumcoi7v',
value: 999999,
},
totalCompensationId: 'cl96stky9003jw32gzumcoi7v',
},
offersFullTimeId: 'cl96stky9003cw32g5v10izfu',
offersIntern: null,
offersInternId: null,
profileId: 'cl96stky5002ew32gx2kale2x',
},
{
comments: 'this IS SO COOL',
company: {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
id: 'cl95u79f000007im531ysjg79',
logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta',
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl9h0bqu50000txxwkhmshhxz',
id: 'cl976wf28000t7iyga4noyz7s',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
negotiationStrategy: 'Charmed the guy with my face',
offersFullTime: {
baseSalary: {
currency: 'SGD',
id: 'cl976wf28000v7iygmk1b7qaq',
value: 1999999999,
},
baseSalaryId: 'cl976wf28000v7iygmk1b7qaq',
bonus: {
currency: 'SGD',
id: 'cl976wf28000x7iyg63w7kcli',
value: 1410065407,
},
bonusId: 'cl976wf28000x7iyg63w7kcli',
id: 'cl976wf28000u7iyg6euei8e9',
level: 'EXPERT',
specialization: 'FRONTEND',
stocks: {
currency: 'SGD',
id: 'cl976wf28000z7iyg9ivun6ap',
value: 111222333,
},
stocksId: 'cl976wf28000z7iyg9ivun6ap',
title: 'Software Engineer',
totalCompensation: {
currency: 'SGD',
id: 'cl976wf2800117iygmzsc0xit',
value: 55555555,
},
totalCompensationId: 'cl976wf2800117iygmzsc0xit',
},
offersFullTimeId: 'cl976wf28000u7iyg6euei8e9',
offersIntern: null,
offersInternId: null,
profileId: 'cl96stky5002ew32gx2kale2x',
},
{
comments: 'this rocks',
company: {
createdAt: new Date('2022-10-12T16:19:05.196Z'),
description:
'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
id: 'cl95u79f000007im531ysjg79',
logoUrl: 'https://logo.clearbit.com/meta.com',
name: 'Meta',
slug: 'meta',
updatedAt: new Date('2022-10-12T16:19:05.196Z'),
},
companyId: 'cl9h0bqu50000txxwkhmshhxz',
id: 'cl96tbb3o0051w32gjrpaiiit',
jobType: 'FULLTIME',
location: 'Singapore, Singapore',
monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
negotiationStrategy: 'Charmed the guy with my face',
offersFullTime: {
baseSalary: {
currency: 'SGD',
id: 'cl96tbb3o0053w32gz11paaxu',
value: 1999999999,
},
baseSalaryId: 'cl96tbb3o0053w32gz11paaxu',
bonus: {
currency: 'SGD',
id: 'cl96tbb3o0055w32gpyqgz5hx',
value: 1410065407,
},
bonusId: 'cl96tbb3o0055w32gpyqgz5hx',
id: 'cl96tbb3o0052w32guguajzin',
level: 'EXPERT',
specialization: 'FRONTEND',
stocks: {
currency: 'SGD',
id: 'cl96tbb3o0057w32gu4nyxguf',
value: 500,
},
stocksId: 'cl96tbb3o0057w32gu4nyxguf',
title: 'Software Engineer',
totalCompensation: {
currency: 'SGD',
id: 'cl96tbb3o0059w32gm3iy1zk4',
value: 55555555,
},
totalCompensationId: 'cl96tbb3o0059w32gm3iy1zk4',
},
offersFullTimeId: 'cl96tbb3o0052w32guguajzin',
offersIntern: null,
offersInternId: null,
profileId: 'cl96stky5002ew32gx2kale2x',
},
profileId: 'cl9i68fv60000tthj8t3zkox0',
},
// {
// comments: '',
// company: {
// createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description:
// 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
// id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta',
// slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// },
// companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl9i68fvf000ytthj0ltsqt1d',
// jobType: 'FULLTIME',
// location: 'Singapore, Singapore',
// monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
// negotiationStrategy: 'Leveraged having million offers',
// offersFullTime: {
// baseSalary: {
// currency: 'SGD',
// id: 'cl9i68fvf0010tthj0iym6woh',
// value: 84000,
// },
// baseSalaryId: 'cl9i68fvf0010tthj0iym6woh',
// bonus: {
// currency: 'SGD',
// id: 'cl9i68fvf0012tthjioltnspk',
// value: 123456789,
// },
// bonusId: 'cl9i68fvf0012tthjioltnspk',
// id: 'cl9i68fvf000ztthjcovbiehc',
// level: 'Junior',
// specialization: 'Front End',
// stocks: {
// currency: 'SGD',
// id: 'cl9i68fvf0014tthjz2gff3hs',
// value: 100,
// },
// stocksId: 'cl9i68fvf0014tthjz2gff3hs',
// title: 'Software Engineer',
// totalCompensation: {
// currency: 'SGD',
// id: 'cl9i68fvf0016tthjrtb7iuvj',
// value: 104100,
// },
// totalCompensationId: 'cl9i68fvf0016tthjrtb7iuvj',
// },
// offersFullTimeId: 'cl9i68fvf000ztthjcovbiehc',
// offersIntern: null,
// offersInternId: null,
// profileId: 'cl9i68fv60000tthj8t3zkox0',
// },
// {
// comments: '',
// company: {
// createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description:
// 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
// id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta',
// slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// },
// companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl96stky9003bw32gc3l955vr',
// jobType: 'FULLTIME',
// location: 'Singapore, Singapore',
// monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
// negotiationStrategy: 'LOst out having multiple offers',
// offersFullTime: {
// baseSalary: {
// currency: 'SGD',
// id: 'cl96stky9003dw32gcvqbijlo',
// value: 1,
// },
// baseSalaryId: 'cl96stky9003dw32gcvqbijlo',
// bonus: {
// currency: 'SGD',
// id: 'cl96stky9003fw32goc3zqxwr',
// value: 0,
// },
// bonusId: 'cl96stky9003fw32goc3zqxwr',
// id: 'cl96stky9003cw32g5v10izfu',
// level: 'Senior',
// specialization: 'Front End',
// stocks: {
// currency: 'SGD',
// id: 'cl96stky9003hw32g1lbbkqqr',
// value: 999999,
// },
// stocksId: 'cl96stky9003hw32g1lbbkqqr',
// title: 'Software Engineer DOG',
// totalCompensation: {
// currency: 'SGD',
// id: 'cl96stky9003jw32gzumcoi7v',
// value: 999999,
// },
// totalCompensationId: 'cl96stky9003jw32gzumcoi7v',
// },
// offersFullTimeId: 'cl96stky9003cw32g5v10izfu',
// offersIntern: null,
// offersInternId: null,
// profileId: 'cl96stky5002ew32gx2kale2x',
// },
// {
// comments: 'this IS SO COOL',
// company: {
// createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description:
// 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
// id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta',
// slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// },
// companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl976wf28000t7iyga4noyz7s',
// jobType: 'FULLTIME',
// location: 'Singapore, Singapore',
// monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
// negotiationStrategy: 'Charmed the guy with my face',
// offersFullTime: {
// baseSalary: {
// currency: 'SGD',
// id: 'cl976wf28000v7iygmk1b7qaq',
// value: 1999999999,
// },
// baseSalaryId: 'cl976wf28000v7iygmk1b7qaq',
// bonus: {
// currency: 'SGD',
// id: 'cl976wf28000x7iyg63w7kcli',
// value: 1410065407,
// },
// bonusId: 'cl976wf28000x7iyg63w7kcli',
// id: 'cl976wf28000u7iyg6euei8e9',
// level: 'EXPERT',
// specialization: 'FRONTEND',
// stocks: {
// currency: 'SGD',
// id: 'cl976wf28000z7iyg9ivun6ap',
// value: 111222333,
// },
// stocksId: 'cl976wf28000z7iyg9ivun6ap',
// title: 'Software Engineer',
// totalCompensation: {
// currency: 'SGD',
// id: 'cl976wf2800117iygmzsc0xit',
// value: 55555555,
// },
// totalCompensationId: 'cl976wf2800117iygmzsc0xit',
// },
// offersFullTimeId: 'cl976wf28000u7iyg6euei8e9',
// offersIntern: null,
// offersInternId: null,
// profileId: 'cl96stky5002ew32gx2kale2x',
// },
// {
// comments: 'this rocks',
// company: {
// createdAt: new Date('2022-10-12T16:19:05.196Z'),
// description:
// 'Meta Platforms, Inc., doing business as Meta and formerly named Facebook, Inc., and TheFacebook, Inc., is an American multinational technology conglomerate based in Menlo Park, California. The company owns Facebook, Instagram, and WhatsApp, among other products and services.',
// id: 'cl9j4yawz0003utlp1uaa1t8o',
// logoUrl: 'https://logo.clearbit.com/meta.com',
// name: 'Meta',
// slug: 'meta',
// updatedAt: new Date('2022-10-12T16:19:05.196Z'),
// },
// companyId: 'cl9j4yawz0003utlp1uaa1t8o',
// id: 'cl96tbb3o0051w32gjrpaiiit',
// jobType: 'FULLTIME',
// location: 'Singapore, Singapore',
// monthYearReceived: new Date('2022-09-30T07:58:54.000Z'),
// negotiationStrategy: 'Charmed the guy with my face',
// offersFullTime: {
// baseSalary: {
// currency: 'SGD',
// id: 'cl96tbb3o0053w32gz11paaxu',
// value: 1999999999,
// },
// baseSalaryId: 'cl96tbb3o0053w32gz11paaxu',
// bonus: {
// currency: 'SGD',
// id: 'cl96tbb3o0055w32gpyqgz5hx',
// value: 1410065407,
// },
// bonusId: 'cl96tbb3o0055w32gpyqgz5hx',
// id: 'cl96tbb3o0052w32guguajzin',
// level: 'EXPERT',
// specialization: 'FRONTEND',
// stocks: {
// currency: 'SGD',
// id: 'cl96tbb3o0057w32gu4nyxguf',
// value: 500,
// },
// stocksId: 'cl96tbb3o0057w32gu4nyxguf',
// title: 'Software Engineer',
// totalCompensation: {
// currency: 'SGD',
// id: 'cl96tbb3o0059w32gm3iy1zk4',
// value: 55555555,
// },
// totalCompensationId: 'cl96tbb3o0059w32gm3iy1zk4',
// },
// offersFullTimeId: 'cl96tbb3o0052w32guguajzin',
// offersIntern: null,
// offersInternId: null,
// profileId: 'cl96stky5002ew32gx2kale2x',
// },
],
profileName: 'ailing bryann stuart ziqing',
token: 'afca11e436d21bde24543718fa957c6c625335439dc504f24ee35eae7b5ef1ba',
// ProfileName: 'ailing bryann stuart ziqing',
token: '24bafa6fef803f447d7f2e229b14cb8ee43f0c22dffbe41ee1c1e5e6e870f117',
userId: null,
});
};

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

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

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

@ -27,6 +27,8 @@ import {
} from '~/utils/questions/useSearchFilter';
import { trpc } from '~/utils/trpc';
import { SortOrder, SortType } from '~/types/questions.d';
export default function QuestionsBrowsePage() {
const router = useRouter();
@ -87,6 +89,8 @@ export default function QuestionsBrowsePage() {
locations: selectedLocations,
questionTypes: selectedQuestionTypes,
roles: selectedRoles,
sortOrder: SortOrder.DESC,
sortType: SortType.NEW,
startDate,
},
],

@ -131,7 +131,9 @@ export default function ResumeReviewPage() {
onClick={onStarButtonClick}>
<span className="relative inline-flex">
<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" />
) : (
<StarIcon
@ -198,7 +200,10 @@ export default function ResumeReviewPage() {
aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400"
/>
<ResumeExpandableText text={detailsQuery.data.additionalInfo} />
<ResumeExpandableText
key={detailsQuery.data.additionalInfo}
text={detailsQuery.data.additionalInfo}
/>
</div>
)}
<div className="flex w-full flex-col py-4 lg:flex-row">

@ -1,9 +1,10 @@
import Head from 'next/head';
import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react';
import { useEffect, useState } from 'react';
import { Disclosure } from '@headlessui/react';
import { MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { Fragment, useEffect, useState } from 'react';
import { Dialog, Disclosure, Transition } from '@headlessui/react';
import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { XMarkIcon } from '@heroicons/react/24/outline';
import {
MagnifyingGlassIcon,
NewspaperIcon,
@ -13,6 +14,7 @@ import {
CheckboxList,
DropdownMenu,
Pagination,
Spinner,
Tabs,
TextInput,
} from '@tih/ui';
@ -61,6 +63,17 @@ const filters: Array<Filter> = [
},
];
const getLoggedOutText = (tabsValue: string) => {
switch (tabsValue) {
case BROWSE_TABS_VALUES.STARRED:
return 'to view starred resumes!';
case BROWSE_TABS_VALUES.MY:
return 'to view your submitted resumes!';
default:
return '';
}
};
const getEmptyDataText = (
tabsValue: string,
searchValue: string,
@ -76,11 +89,11 @@ const getEmptyDataText = (
case BROWSE_TABS_VALUES.ALL:
return 'Looks like SWEs are feeling lucky!';
case BROWSE_TABS_VALUES.STARRED:
return 'You have not starred any resumes.\nStar one to see it here!';
return 'You have not starred any resumes. Star one to see it here!';
case BROWSE_TABS_VALUES.MY:
return 'Upload a resume to see it here!';
default:
return null;
return '';
}
};
@ -92,9 +105,8 @@ export default function ResumeHomePage() {
const [searchValue, setSearchValue] = useState('');
const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE);
const [shortcutSelected, setShortcutSelected] = useState('All');
const [renderSignInButton, setRenderSignInButton] = useState(false);
const [signInButtonText, setSignInButtonText] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const skip = (currentPage - 1) * PAGE_LIMIT;
@ -117,9 +129,6 @@ export default function ResumeHomePage() {
],
{
enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
onSuccess: () => {
setRenderSignInButton(false);
},
staleTime: 5 * 60 * 1000,
},
);
@ -138,10 +147,6 @@ export default function ResumeHomePage() {
],
{
enabled: tabsValue === BROWSE_TABS_VALUES.STARRED,
onError: () => {
setRenderSignInButton(true);
setSignInButtonText('to view starred resumes');
},
retry: false,
staleTime: 5 * 60 * 1000,
},
@ -161,20 +166,16 @@ export default function ResumeHomePage() {
],
{
enabled: tabsValue === BROWSE_TABS_VALUES.MY,
onError: () => {
setRenderSignInButton(true);
setSignInButtonText('to view your submitted resumes');
},
retry: false,
staleTime: 5 * 60 * 1000,
},
);
const onSubmitResume = () => {
if (sessionData?.user?.id) {
router.push('/resumes/submit');
} else {
if (sessionData === null) {
router.push('/api/auth/signin');
} else {
router.push('/resumes/submit');
}
};
@ -242,86 +243,137 @@ export default function ResumeHomePage() {
<Head>
<title>Resume Review Portal</title>
</Head>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-scroll">
<div className="ml-4 py-4">
<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>
{/* Mobile Filters */}
<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
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"
onClick={onSubmitResume}>
Submit Resume
onClick={() => setMobileFiltersOpen(false)}>
<span className="sr-only">Close menu</span>
<XMarkIcon aria-hidden="true" className="h-6 w-6" />
</button>
</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>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</Dialog.Panel>
</Transition.Child>
</div>
</Dialog>
</Transition.Root>
</div>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-auto">
<div className="ml-4 py-4">
<ResumeReviewsTitle />
</div>
<div className="grid grid-cols-12">
<div className="col-span-2">
<div className="mx-8 mt-4 flex justify-start">
<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">
<form>
<h3 className="sr-only">Shortcuts</h3>
<ul
className="flex flex-wrap justify-start gap-4 pb-6 text-sm font-medium text-gray-900"
role="list">
@ -399,11 +451,98 @@ export default function ResumeHomePage() {
</form>
</div>
</div>
<div className="col-span-10 mb-6">
{renderSignInButton && (
<ResumeSignInButton text={signInButtonText} />
)}
{getTabResumes().length === 0 ? (
<div className="w-full">
<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 ? (
<ResumeSignInButton
className="mt-8"
text={getLoggedOutText(tabsValue)}
/>
) : getTabResumes().length === 0 ? (
<div className="mt-24 flex flex-wrap justify-center">
<NewspaperIcon
className="mb-12 basis-full"
@ -436,7 +575,6 @@ export default function ResumeHomePage() {
</div>
</div>
</div>
</div>
</main>
</>
);

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

@ -187,14 +187,14 @@ export const offersAnalysisRouter = createRouter()
{
offersFullTime: {
totalCompensation: {
value: 'desc',
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
value: 'desc',
baseValue: 'desc',
},
},
},
@ -216,15 +216,17 @@ export const offersAnalysisRouter = createRouter()
// TODO: Shift yoe out of background to make it mandatory
if (
!overallHighestOffer.profile.background ||
overallHighestOffer.profile.background.totalYoe === undefined
overallHighestOffer.profile.background.totalYoe == null
) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Cannot analyse without YOE',
code: 'NOT_FOUND',
message: 'YOE not found',
});
}
const yoe = overallHighestOffer.profile.background.totalYoe as number;
const monthYearReceived = new Date(overallHighestOffer.monthYearReceived);
monthYearReceived.setFullYear(monthYearReceived.getFullYear() - 1);
let similarOffers = await ctx.prisma.offersOffer.findMany({
include: {
@ -257,14 +259,14 @@ export const offersAnalysisRouter = createRouter()
{
offersFullTime: {
totalCompensation: {
value: 'desc',
baseValue: 'desc',
},
},
},
{
offersIntern: {
monthlySalary: {
value: 'desc',
baseValue: 'desc',
},
},
},
@ -274,17 +276,20 @@ export const offersAnalysisRouter = createRouter()
{
location: overallHighestOffer.location,
},
{
monthYearReceived: {
gte: monthYearReceived,
},
},
{
OR: [
{
offersFullTime: {
level: overallHighestOffer.offersFullTime?.level,
specialization:
overallHighestOffer.offersFullTime?.specialization,
title: overallHighestOffer.offersFullTime?.title,
},
offersIntern: {
specialization:
overallHighestOffer.offersIntern?.specialization,
title: overallHighestOffer.offersIntern?.title,
},
},
],
@ -317,7 +322,9 @@ export const offersAnalysisRouter = createRouter()
similarOffers,
);
const overallPercentile =
similarOffers.length === 0 ? 0 : overallIndex / similarOffers.length;
similarOffers.length === 0
? 100
: (100 * overallIndex) / similarOffers.length;
const companyIndex = searchOfferPercentile(
overallHighestOffer,
@ -325,10 +332,11 @@ export const offersAnalysisRouter = createRouter()
);
const companyPercentile =
similarCompanyOffers.length === 0
? 0
: companyIndex / similarCompanyOffers.length;
? 100
: (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(
(offer) => offer.id !== overallHighestOffer.id,
);
@ -337,10 +345,9 @@ export const offersAnalysisRouter = createRouter()
);
const noOfSimilarOffers = similarOffers.length;
const similarOffers90PercentileIndex =
Math.floor(noOfSimilarOffers * 0.9) - 1;
const similarOffers90PercentileIndex = Math.ceil(noOfSimilarOffers * 0.1);
const topPercentileOffers =
noOfSimilarOffers > 1
noOfSimilarOffers > 2
? similarOffers.slice(
similarOffers90PercentileIndex,
similarOffers90PercentileIndex + 2,
@ -348,10 +355,11 @@ export const offersAnalysisRouter = createRouter()
: similarOffers;
const noOfSimilarCompanyOffers = similarCompanyOffers.length;
const similarCompanyOffers90PercentileIndex =
Math.floor(noOfSimilarCompanyOffers * 0.9) - 1;
const similarCompanyOffers90PercentileIndex = Math.ceil(
noOfSimilarCompanyOffers * 0.1,
);
const topPercentileCompanyOffers =
noOfSimilarCompanyOffers > 1
noOfSimilarCompanyOffers > 2
? similarCompanyOffers.slice(
similarCompanyOffers90PercentileIndex,
similarCompanyOffers90PercentileIndex + 2,

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

@ -1,5 +1,6 @@
import crypto, { randomUUID } from 'crypto';
import { z } from 'zod';
import { JobType } from '@prisma/client';
import * as trpc from '@trpc/server';
import {
@ -7,6 +8,9 @@ import {
createOfferProfileResponseMapper,
profileDtoMapper,
} 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';
@ -31,7 +35,7 @@ const offer = z.object({
company: company.nullish(),
companyId: z.string(),
id: z.string().optional(),
jobType: z.string(),
jobType: z.string().regex(createValidationRegex(Object.keys(JobType), null)),
location: z.string(),
monthYearReceived: z.date(),
negotiationStrategy: z.string(),
@ -73,7 +77,10 @@ const experience = z.object({
companyId: z.string().nullish(),
durationInMonths: z.number().nullish(),
id: z.string().optional(),
jobType: z.string().nullish(),
jobType: z
.string()
.regex(createValidationRegex(Object.keys(JobType), null))
.nullish(),
level: z.string().nullish(),
location: z.string().nullish(),
monthlySalary: valuation.nullish(),
@ -94,15 +101,6 @@ const education = z.object({
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()
.query('listOne', {
input: z.object({
@ -282,11 +280,11 @@ export const offersProfileRouter = createRouter()
})),
},
experiences: {
create: input.background.experiences.map((x) => {
create: input.background.experiences.map(async (x) => {
if (
x.jobType === 'FULLTIME' &&
x.totalCompensation?.currency !== undefined &&
x.totalCompensation.value !== undefined
x.jobType === JobType.FULLTIME &&
x.totalCompensation?.currency != null &&
x.totalCompensation?.value != null
) {
if (x.companyId) {
return {
@ -302,8 +300,14 @@ export const offersProfileRouter = createRouter()
title: x.title,
totalCompensation: {
create: {
currency: x.totalCompensation?.currency,
value: x.totalCompensation?.value,
baseCurrency: baseCurrencyString,
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,
jobType: x.jobType,
level: x.level,
location: x.location,
specialization: x.specialization,
title: x.title,
totalCompensation: {
create: {
currency: x.totalCompensation?.currency,
value: x.totalCompensation?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
x.totalCompensation.value,
x.totalCompensation.currency,
baseCurrencyString,
),
currency: x.totalCompensation.currency,
value: x.totalCompensation.value,
},
},
};
}
if (
x.jobType === 'INTERN' &&
x.monthlySalary?.currency !== undefined &&
x.monthlySalary.value !== undefined
x.jobType === JobType.INTERN &&
x.monthlySalary?.currency != null &&
x.monthlySalary?.value != null
) {
if (x.companyId) {
return {
@ -338,8 +349,14 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType,
monthlySalary: {
create: {
currency: x.monthlySalary?.currency,
value: x.monthlySalary?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
x.monthlySalary.value,
x.monthlySalary.currency,
baseCurrencyString,
),
currency: x.monthlySalary.currency,
value: x.monthlySalary.value,
},
},
specialization: x.specialization,
@ -351,8 +368,14 @@ export const offersProfileRouter = createRouter()
jobType: x.jobType,
monthlySalary: {
create: {
currency: x.monthlySalary?.currency,
value: x.monthlySalary?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
x.monthlySalary.value,
x.monthlySalary.currency,
baseCurrencyString,
),
currency: x.monthlySalary.currency,
value: x.monthlySalary.value,
},
},
specialization: x.specialization,
@ -379,14 +402,15 @@ export const offersProfileRouter = createRouter()
},
editToken: token,
offers: {
create: input.offers.map((x) => {
create: await Promise.all(
input.offers.map(async (x) => {
if (
x.jobType === 'INTERN' &&
x.jobType === JobType.INTERN &&
x.offersIntern &&
x.offersIntern.internshipCycle &&
x.offersIntern.monthlySalary?.currency &&
x.offersIntern.monthlySalary.value &&
x.offersIntern.startYear
x.offersIntern.internshipCycle != null &&
x.offersIntern.monthlySalary?.currency != null &&
x.offersIntern.monthlySalary?.value != null &&
x.offersIntern.startYear != null
) {
return {
comments: x.comments,
@ -404,8 +428,14 @@ export const offersProfileRouter = createRouter()
internshipCycle: x.offersIntern.internshipCycle,
monthlySalary: {
create: {
currency: x.offersIntern.monthlySalary?.currency,
value: x.offersIntern.monthlySalary?.value,
baseCurrency: baseCurrencyString,
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,
@ -416,17 +446,19 @@ export const offersProfileRouter = createRouter()
};
}
if (
x.jobType === 'FULLTIME' &&
x.jobType === JobType.FULLTIME &&
x.offersFullTime &&
x.offersFullTime.baseSalary?.currency &&
x.offersFullTime.baseSalary?.value &&
x.offersFullTime.bonus?.currency &&
x.offersFullTime.bonus?.value &&
x.offersFullTime.stocks?.currency &&
x.offersFullTime.stocks?.value &&
x.offersFullTime.totalCompensation?.currency &&
x.offersFullTime.totalCompensation?.value &&
x.offersFullTime.level
x.offersFullTime.baseSalary?.currency != null &&
x.offersFullTime.baseSalary?.value != null &&
x.offersFullTime.bonus?.currency != null &&
x.offersFullTime.bonus?.value != null &&
x.offersFullTime.stocks?.currency != null &&
x.offersFullTime.stocks?.value != null &&
x.offersFullTime.totalCompensation?.currency != null &&
x.offersFullTime.totalCompensation?.value != null &&
x.offersFullTime.level != null &&
x.offersFullTime.title != null &&
x.offersFullTime.specialization != null
) {
return {
comments: x.comments,
@ -443,30 +475,54 @@ export const offersProfileRouter = createRouter()
create: {
baseSalary: {
create: {
currency: x.offersFullTime.baseSalary?.currency,
value: x.offersFullTime.baseSalary?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
x.offersFullTime.baseSalary.value,
x.offersFullTime.baseSalary.currency,
baseCurrencyString,
),
currency: x.offersFullTime.baseSalary.currency,
value: x.offersFullTime.baseSalary.value,
},
},
bonus: {
create: {
currency: x.offersFullTime.bonus?.currency,
value: x.offersFullTime.bonus?.value,
baseCurrency: baseCurrencyString,
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,
specialization: x.offersFullTime.specialization,
stocks: {
create: {
currency: x.offersFullTime.stocks?.currency,
value: x.offersFullTime.stocks?.value,
baseCurrency: baseCurrencyString,
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,
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
x.offersFullTime.totalCompensation.value,
x.offersFullTime.totalCompensation.currency,
baseCurrencyString,
),
currency:
x.offersFullTime.totalCompensation?.currency,
value: x.offersFullTime.totalCompensation?.value,
x.offersFullTime.totalCompensation.currency,
value: x.offersFullTime.totalCompensation.value,
},
},
},
@ -480,6 +536,7 @@ export const offersProfileRouter = createRouter()
message: 'Missing fields.',
});
}),
),
},
profileName: randomUUID().substring(0, 10),
},
@ -510,7 +567,7 @@ export const offersProfileRouter = createRouter()
return deletedProfile.id;
}
// TODO: Throw 401
throw new trpc.TRPCError({
code: 'UNAUTHORIZED',
message: 'Invalid token.',
@ -535,11 +592,10 @@ export const offersProfileRouter = createRouter()
totalYoe: z.number(),
}),
createdAt: z.string().optional(),
discussion: z.array(reply),
id: z.string(),
isEditable: z.boolean().nullish(),
offers: z.array(offer),
profileName: z.string(),
profileName: z.string().optional(),
token: z.string(),
userId: z.string().nullish(),
}),
@ -552,6 +608,7 @@ export const offersProfileRouter = createRouter()
const profileEditToken = profileToUpdate?.editToken;
if (profileEditToken === input.token) {
if (input.profileName) {
await ctx.prisma.offersProfile.update({
data: {
profileName: input.profileName,
@ -560,6 +617,7 @@ export const offersProfileRouter = createRouter()
id: input.id,
},
});
}
await ctx.prisma.offersBackground.update({
data: {
@ -570,8 +628,28 @@ export const offersProfileRouter = createRouter()
},
});
// Delete educations
const educationsId = (
await ctx.prisma.offersEducation.findMany({
where: {
backgroundId: input.background.id,
},
})
).map((x) => x.id);
for (const id of educationsId) {
if (!input.background.educations.map((x) => x.id).includes(id)) {
await ctx.prisma.offersEducation.delete({
where: {
id,
},
});
}
}
for (const edu of input.background.educations) {
if (edu.id) {
// Update existing education
await ctx.prisma.offersEducation.update({
data: {
endDate: edu.endDate,
@ -585,6 +663,7 @@ export const offersProfileRouter = createRouter()
},
});
} else {
// Create new education
await ctx.prisma.offersBackground.update({
data: {
educations: {
@ -604,8 +683,28 @@ export const offersProfileRouter = createRouter()
}
}
// Delete experiences
const experiencesId = (
await ctx.prisma.offersExperience.findMany({
where: {
backgroundId: input.background.id,
},
})
).map((x) => x.id);
for (const id of experiencesId) {
if (!input.background.experiences.map((x) => x.id).includes(id)) {
await ctx.prisma.offersExperience.delete({
where: {
id,
},
});
}
}
for (const exp of input.background.experiences) {
if (exp.id) {
// Update existing experience
await ctx.prisma.offersExperience.update({
data: {
companyId: exp.companyId,
@ -621,6 +720,12 @@ export const offersProfileRouter = createRouter()
if (exp.monthlySalary) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
@ -633,6 +738,12 @@ export const offersProfileRouter = createRouter()
if (exp.totalCompensation) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
@ -642,10 +753,11 @@ export const offersProfileRouter = createRouter()
});
}
} else if (!exp.id) {
// Create new experience
if (
exp.jobType === 'FULLTIME' &&
exp.totalCompensation?.currency !== undefined &&
exp.totalCompensation.value !== undefined
exp.jobType === JobType.FULLTIME &&
exp.totalCompensation?.currency != null &&
exp.totalCompensation?.value != null
) {
if (exp.companyId) {
await ctx.prisma.offersBackground.update({
@ -660,12 +772,19 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
specialization: exp.specialization,
title: exp.title,
totalCompensation: {
create: {
currency: exp.totalCompensation?.currency,
value: exp.totalCompensation?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
},
@ -683,12 +802,19 @@ export const offersProfileRouter = createRouter()
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
level: exp.level,
location: exp.location,
specialization: exp.specialization,
title: exp.title,
totalCompensation: {
create: {
currency: exp.totalCompensation?.currency,
value: exp.totalCompensation?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.totalCompensation.value,
exp.totalCompensation.currency,
baseCurrencyString,
),
currency: exp.totalCompensation.currency,
value: exp.totalCompensation.value,
},
},
},
@ -700,9 +826,9 @@ export const offersProfileRouter = createRouter()
});
}
} else if (
exp.jobType === 'INTERN' &&
exp.monthlySalary?.currency !== undefined &&
exp.monthlySalary.value !== undefined
exp.jobType === JobType.INTERN &&
exp.monthlySalary?.currency != null &&
exp.monthlySalary?.value != null
) {
if (exp.companyId) {
await ctx.prisma.offersBackground.update({
@ -716,10 +842,17 @@ export const offersProfileRouter = createRouter()
},
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
monthlySalary: {
create: {
currency: exp.monthlySalary?.currency,
value: exp.monthlySalary?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
specialization: exp.specialization,
@ -738,10 +871,17 @@ export const offersProfileRouter = createRouter()
create: {
durationInMonths: exp.durationInMonths,
jobType: exp.jobType,
location: exp.location,
monthlySalary: {
create: {
currency: exp.monthlySalary?.currency,
value: exp.monthlySalary?.value,
baseCurrency: baseCurrencyString,
baseValue: await convert(
exp.monthlySalary.value,
exp.monthlySalary.currency,
baseCurrencyString,
),
currency: exp.monthlySalary.currency,
value: exp.monthlySalary.value,
},
},
specialization: exp.specialization,
@ -758,8 +898,28 @@ export const offersProfileRouter = createRouter()
}
}
// Delete specific yoes
const yoesId = (
await ctx.prisma.offersSpecificYoe.findMany({
where: {
backgroundId: input.background.id,
},
})
).map((x) => x.id);
for (const id of yoesId) {
if (!input.background.specificYoes.map((x) => x.id).includes(id)) {
await ctx.prisma.offersSpecificYoe.delete({
where: {
id,
},
});
}
}
for (const yoe of input.background.specificYoes) {
if (yoe.id) {
// Update existing yoe
await ctx.prisma.offersSpecificYoe.update({
data: {
...yoe,
@ -769,6 +929,7 @@ export const offersProfileRouter = createRouter()
},
});
} else {
// Create new yoe
await ctx.prisma.offersBackground.update({
data: {
specificYoes: {
@ -785,12 +946,37 @@ export const offersProfileRouter = createRouter()
}
}
// Delete specific offers
const offers = (
await ctx.prisma.offersOffer.findMany({
where: {
profileId: input.id,
},
})
).map((x) => x.id);
for (const id of offers) {
if (!input.offers.map((x) => x.id).includes(id)) {
await ctx.prisma.offersOffer.delete({
where: {
id,
},
});
}
}
// Update remaining offers
for (const offerToUpdate of input.offers) {
if (offerToUpdate.id) {
// Update existing offer
await ctx.prisma.offersOffer.update({
data: {
comments: offerToUpdate.comments,
companyId: offerToUpdate.companyId,
jobType:
offerToUpdate.jobType === JobType.FULLTIME
? JobType.FULLTIME
: JobType.INTERN,
location: offerToUpdate.location,
monthYearReceived: offerToUpdate.monthYearReceived,
negotiationStrategy: offerToUpdate.negotiationStrategy,
@ -800,21 +986,7 @@ export const offersProfileRouter = createRouter()
},
});
if (
offerToUpdate.jobType === 'INTERN' ||
offerToUpdate.jobType === 'FULLTIME'
) {
await ctx.prisma.offersOffer.update({
data: {
jobType: offerToUpdate.jobType,
},
where: {
id: offerToUpdate.id,
},
});
}
if (offerToUpdate.offersIntern?.monthlySalary) {
if (offerToUpdate.offersIntern?.monthlySalary != null) {
await ctx.prisma.offersIntern.update({
data: {
internshipCycle:
@ -829,6 +1001,12 @@ export const offersProfileRouter = createRouter()
});
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersIntern.monthlySalary.value,
offerToUpdate.offersIntern.monthlySalary.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersIntern.monthlySalary.currency,
value: offerToUpdate.offersIntern.monthlySalary.value,
},
@ -838,7 +1016,7 @@ export const offersProfileRouter = createRouter()
});
}
if (offerToUpdate.offersFullTime?.totalCompensation) {
if (offerToUpdate.offersFullTime?.totalCompensation != null) {
await ctx.prisma.offersFullTime.update({
data: {
level: offerToUpdate.offersFullTime.level ?? undefined,
@ -849,9 +1027,15 @@ export const offersProfileRouter = createRouter()
id: offerToUpdate.offersFullTime.id,
},
});
if (offerToUpdate.offersFullTime.baseSalary) {
if (offerToUpdate.offersFullTime.baseSalary != null) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.baseSalary.value,
offerToUpdate.offersFullTime.baseSalary.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.baseSalary.currency,
value: offerToUpdate.offersFullTime.baseSalary.value,
},
@ -863,6 +1047,12 @@ export const offersProfileRouter = createRouter()
if (offerToUpdate.offersFullTime.bonus) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.bonus.value,
offerToUpdate.offersFullTime.bonus.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.bonus.currency,
value: offerToUpdate.offersFullTime.bonus.value,
},
@ -874,6 +1064,12 @@ export const offersProfileRouter = createRouter()
if (offerToUpdate.offersFullTime.stocks) {
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.stocks.value,
offerToUpdate.offersFullTime.stocks.currency,
baseCurrencyString,
),
currency: offerToUpdate.offersFullTime.stocks.currency,
value: offerToUpdate.offersFullTime.stocks.value,
},
@ -884,6 +1080,12 @@ export const offersProfileRouter = createRouter()
}
await ctx.prisma.offersCurrency.update({
data: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.totalCompensation.value,
offerToUpdate.offersFullTime.totalCompensation.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.totalCompensation.currency,
value: offerToUpdate.offersFullTime.totalCompensation.value,
@ -894,13 +1096,14 @@ export const offersProfileRouter = createRouter()
});
}
} else {
// Create new offer
if (
offerToUpdate.jobType === 'INTERN' &&
offerToUpdate.jobType === JobType.INTERN &&
offerToUpdate.offersIntern &&
offerToUpdate.offersIntern.internshipCycle &&
offerToUpdate.offersIntern.monthlySalary?.currency &&
offerToUpdate.offersIntern.monthlySalary.value &&
offerToUpdate.offersIntern.startYear
offerToUpdate.offersIntern.internshipCycle != null &&
offerToUpdate.offersIntern.monthlySalary?.currency != null &&
offerToUpdate.offersIntern.monthlySalary?.value != null &&
offerToUpdate.offersIntern.startYear != null
) {
await ctx.prisma.offersProfile.update({
data: {
@ -922,11 +1125,18 @@ export const offersProfileRouter = createRouter()
offerToUpdate.offersIntern.internshipCycle,
monthlySalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersIntern.monthlySalary.value,
offerToUpdate.offersIntern.monthlySalary
.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersIntern.monthlySalary
?.currency,
.currency,
value:
offerToUpdate.offersIntern.monthlySalary?.value,
offerToUpdate.offersIntern.monthlySalary.value,
},
},
specialization:
@ -944,17 +1154,18 @@ export const offersProfileRouter = createRouter()
});
}
if (
offerToUpdate.jobType === 'FULLTIME' &&
offerToUpdate.jobType === JobType.FULLTIME &&
offerToUpdate.offersFullTime &&
offerToUpdate.offersFullTime.baseSalary?.currency &&
offerToUpdate.offersFullTime.baseSalary?.value &&
offerToUpdate.offersFullTime.bonus?.currency &&
offerToUpdate.offersFullTime.bonus?.value &&
offerToUpdate.offersFullTime.stocks?.currency &&
offerToUpdate.offersFullTime.stocks?.value &&
offerToUpdate.offersFullTime.totalCompensation?.currency &&
offerToUpdate.offersFullTime.totalCompensation?.value &&
offerToUpdate.offersFullTime.level
offerToUpdate.offersFullTime.baseSalary?.currency != null &&
offerToUpdate.offersFullTime.baseSalary?.value != null &&
offerToUpdate.offersFullTime.bonus?.currency != null &&
offerToUpdate.offersFullTime.bonus?.value != null &&
offerToUpdate.offersFullTime.stocks?.currency != null &&
offerToUpdate.offersFullTime.stocks?.value != null &&
offerToUpdate.offersFullTime.totalCompensation?.currency !=
null &&
offerToUpdate.offersFullTime.totalCompensation?.value != null &&
offerToUpdate.offersFullTime.level != null
) {
await ctx.prisma.offersProfile.update({
data: {
@ -974,18 +1185,31 @@ export const offersProfileRouter = createRouter()
create: {
baseSalary: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.baseSalary.value,
offerToUpdate.offersFullTime.baseSalary
.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.baseSalary
?.currency,
.currency,
value:
offerToUpdate.offersFullTime.baseSalary?.value,
offerToUpdate.offersFullTime.baseSalary.value,
},
},
bonus: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.bonus.value,
offerToUpdate.offersFullTime.bonus.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.bonus?.currency,
value: offerToUpdate.offersFullTime.bonus?.value,
offerToUpdate.offersFullTime.bonus.currency,
value: offerToUpdate.offersFullTime.bonus.value,
},
},
level: offerToUpdate.offersFullTime.level,
@ -993,20 +1217,34 @@ export const offersProfileRouter = createRouter()
offerToUpdate.offersFullTime.specialization,
stocks: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.stocks.value,
offerToUpdate.offersFullTime.stocks.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.stocks?.currency,
value: offerToUpdate.offersFullTime.stocks?.value,
offerToUpdate.offersFullTime.stocks.currency,
value: offerToUpdate.offersFullTime.stocks.value,
},
},
title: offerToUpdate.offersFullTime.title,
totalCompensation: {
create: {
baseCurrency: baseCurrencyString,
baseValue: await convert(
offerToUpdate.offersFullTime.totalCompensation
.value,
offerToUpdate.offersFullTime.totalCompensation
.currency,
baseCurrencyString,
),
currency:
offerToUpdate.offersFullTime.totalCompensation
?.currency,
.currency,
value:
offerToUpdate.offersFullTime.totalCompensation
?.value,
.value,
},
},
},
@ -1023,46 +1261,6 @@ export const offersProfileRouter = createRouter()
}
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: {
id: input.id,
},

@ -5,9 +5,25 @@ import {
dashboardOfferDtoMapper,
getOffersResponseMapper,
} 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';
const getOrder = (prefix: string) => {
if (prefix === '+') {
return 'asc';
}
return 'desc';
};
const sortingKeysMap = {
monthYearReceived: 'monthYearReceived',
totalCompensation: 'totalCompensation',
totalYoe: 'totalYoe',
};
const yoeCategoryMap: Record<number, string> = {
0: 'Internship',
1: 'Fresh Grad',
@ -25,19 +41,10 @@ const getYoeRange = (yoeCategory: number) => {
: 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', {
input: z.object({
companyId: z.string().nullish(),
currency: z.string().nullish(),
dateEnd: z.date().nullish(),
dateStart: z.date().nullish(),
limit: z.number().positive(),
@ -45,7 +52,10 @@ export const offersRouter = createRouter().query('list', {
offset: z.number().nonnegative(),
salaryMax: 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(),
yoeCategory: z.number().min(0).max(3),
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 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
? await ctx.prisma.offersOffer.findMany({
// 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: {
AND: [
{
location: input.location,
location:
input.location.length === 0 ? undefined : input.location,
},
{
offersIntern: {
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: {
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: {
AND: [
{
location: input.location,
location:
input.location.length === 0 ? undefined : input.location,
},
{
offersIntern: {
@ -136,6 +239,30 @@ export const offersRouter = createRouter().query('list', {
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: {
background: {
@ -146,165 +273,70 @@ export const offersRouter = createRouter().query('list', {
},
},
},
{
monthYearReceived: {
gte: input.dateStart ?? undefined,
lte: input.dateEnd ?? undefined,
},
},
],
},
});
// FILTERING
data = data.filter((offer) => {
let validRecord = true;
if (input.companyId && input.companyId.length !== 0) {
validRecord = validRecord && offer.company.id === input.companyId;
}
if (input.title && input.title.length !== 0) {
validRecord =
validRecord &&
(offer.offersFullTime?.title === input.title ||
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()
// CONVERTING
const currency = input.currency?.toUpperCase();
if (currency != null && currency in Currency) {
data = await Promise.all(
data.map(async (offer) => {
if (offer.offersFullTime?.totalCompensation != null) {
offer.offersFullTime.totalCompensation.value =
await convertWithDate(
offer.offersFullTime.totalCompensation.value,
offer.offersFullTime.totalCompensation.currency,
currency,
offer.offersFullTime.totalCompensation.updatedAt,
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.offersFullTime?.totalCompensation.value
? offer1.offersFullTime?.totalCompensation.value
: offer1.offersIntern?.monthlySalary.value;
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.totalCompensation.currency = currency;
offer.offersFullTime.baseSalary.value = await convertWithDate(
offer.offersFullTime.baseSalary.value,
offer.offersFullTime.baseSalary.currency,
currency,
offer.offersFullTime.baseSalary.updatedAt,
);
}
if (sortingKey === 'totalCompensation') {
const salary1 = offer1.offersFullTime?.totalCompensation.value
? offer1.offersFullTime?.totalCompensation.value
: offer1.offersIntern?.monthlySalary.value;
const salary2 = offer2.offersFullTime?.totalCompensation.value
? offer2.offersFullTime?.totalCompensation.value
: offer2.offersIntern?.monthlySalary.value;
if (salary1 == null || salary2 == null) {
offer.offersFullTime.baseSalary.currency = currency;
offer.offersFullTime.stocks.value = await convertWithDate(
offer.offersFullTime.stocks.value,
offer.offersFullTime.stocks.currency,
currency,
offer.offersFullTime.stocks.updatedAt,
);
offer.offersFullTime.stocks.currency = currency;
offer.offersFullTime.bonus.value = await convertWithDate(
offer.offersFullTime.bonus.value,
offer.offersFullTime.bonus.currency,
currency,
offer.offersFullTime.bonus.updatedAt,
);
offer.offersFullTime.bonus.currency = currency;
} 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({
code: 'NOT_FOUND',
message: 'Total Compensation or Salary not found',
});
}
return salary2 - salary1;
}
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 offer;
}),
);
}
return defaultReturn;
});
const startRecordIndex: number = input.limit * input.offset;
const endRecordIndex: number =

@ -5,18 +5,33 @@ import { TRPCError } from '@trpc/server';
import { createProtectedRouter } from './context';
import type { Question } from '~/types/questions';
import { SortOrder, SortType } from '~/types/questions.d';
const TWO_WEEK_IN_MS = 12096e5;
export const questionsQuestionRouter = createProtectedRouter()
.query('getQuestionsByFilter', {
input: z.object({
companyNames: z.string().array(),
endDate: z.date(),
endDate: z.date().default(new Date()),
locations: z.string().array(),
pageSize: z.number().default(50),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().array(),
startDate: z.date().optional(),
sortOrder: z.nativeEnum(SortOrder),
sortType: z.nativeEnum(SortType),
startDate: z.date().default(new Date(Date.now() - TWO_WEEK_IN_MS)),
}),
async resolve({ ctx, input }) {
const sortCondition =
input.sortType === SortType.TOP
? {
upvotes: input.sortOrder,
}
: {
lastSeenAt: input.sortOrder,
};
const questionsData = await ctx.prisma.questionsQuestion.findMany({
include: {
_count: {
@ -41,7 +56,7 @@ export const questionsQuestionRouter = createProtectedRouter()
votes: true,
},
orderBy: {
createdAt: 'desc',
...sortCondition,
},
where: {
...(input.questionTypes.length > 0
@ -53,6 +68,10 @@ export const questionsQuestionRouter = createProtectedRouter()
: {}),
encounters: {
some: {
seenAt: {
gte: input.startDate,
lte: input.endDate,
},
...(input.companyNames.length > 0
? {
company: {
@ -207,8 +226,7 @@ export const questionsQuestionRouter = createProtectedRouter()
data: {
content: input.content,
encounters: {
create: [
{
create: {
company: {
connect: {
id: input.companyId,
@ -223,8 +241,8 @@ export const questionsQuestionRouter = createProtectedRouter()
},
},
},
],
},
lastSeenAt: input.seenAt,
questionType: input.questionType,
userId,
},
@ -319,13 +337,28 @@ export const questionsQuestionRouter = createProtectedRouter()
const userId = ctx.session?.user?.id;
const { questionId, vote } = input;
return await ctx.prisma.questionsQuestionVote.create({
const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.create({
data: {
questionId,
userId,
vote,
},
});
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: questionId,
},
}),
]);
return questionVote;
},
})
.mutation('updateVote', {
@ -350,14 +383,30 @@ export const questionsQuestionRouter = createProtectedRouter()
});
}
return await ctx.prisma.questionsQuestionVote.update({
const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.update({
data: {
vote,
},
where: {
id,
},
});
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: voteToUpdate.questionId,
},
}),
]);
return questionVote;
},
})
.mutation('deleteVote', {
@ -380,10 +429,25 @@ export const questionsQuestionRouter = createProtectedRouter()
});
}
return await ctx.prisma.questionsQuestionVote.delete({
const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
const [questionVote] = await ctx.prisma.$transaction([
ctx.prisma.questionsQuestionVote.delete({
where: {
id: input.id,
},
});
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: voteToDelete.questionId,
},
}),
]);
return questionVote;
},
});

@ -15,6 +15,19 @@ export const resumeCommentsRouter = createRouter().query('list', {
// The user's name and image to render
const comments = await ctx.prisma.resumesComment.findMany({
include: {
children: {
include: {
user: {
select: {
image: true,
name: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
},
user: {
select: {
image: true,
@ -26,15 +39,35 @@ export const resumeCommentsRouter = createRouter().query('list', {
createdAt: 'desc',
},
where: {
resumeId,
AND: [{ resumeId }, { parentId: null }],
},
});
return comments.map((data) => {
const children: Array<ResumeComment> = data.children.map((child) => {
return {
children: [],
createdAt: child.createdAt,
description: child.description,
id: child.id,
parentId: data.id,
resumeId: child.resumeId,
section: child.section,
updatedAt: child.updatedAt,
user: {
image: child.user.image,
name: child.user.name,
userId: child.userId,
},
};
});
const comment: ResumeComment = {
children,
createdAt: data.createdAt,
description: data.description,
id: data.id,
parentId: data.parentId,
resumeId: data.resumeId,
section: data.section,
updatedAt: data.updatedAt,

@ -67,4 +67,26 @@ export const resumesCommentsUserRouter = createProtectedRouter()
},
});
},
})
.mutation('reply', {
input: z.object({
description: z.string(),
parentId: z.string(),
resumeId: z.string(),
section: z.nativeEnum(ResumesSection),
}),
async resolve({ ctx, input }) {
const userId = ctx.session.user.id;
const { description, parentId, resumeId, section } = input;
return await ctx.prisma.resumesComment.create({
data: {
description,
parentId,
resumeId,
section,
userId,
},
});
},
});

@ -213,7 +213,8 @@ export const resumesRouter = createRouter()
let topUpvotedCommentCount = 0;
for (const resume of resumes) {
let highestVoteCount = 1;
// Set minimum upvote count >= 5 to qualify
let highestVoteCount = 5;
// Get Map of {userId, voteCount} for each comment
const commentUpvotePairs = [];

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

@ -51,3 +51,13 @@ export type QuestionComment = {
user: string;
userImage: string;
};
export enum SortOrder {
ASC = 'asc',
DESC = 'desc',
}
export enum SortType {
TOP,
NEW,
}

@ -5,9 +5,11 @@ import type { ResumesCommentVote, ResumesSection } from '@prisma/client';
* frontend-friendly representation of the query
*/
export type ResumeComment = Readonly<{
children: Array<ResumeComment>;
createdAt: Date;
description: string;
id: string;
parentId: string?;
resumeId: string;
section: ResumesSection;
updatedAt: Date;

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

@ -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 { Currency } from './CurrencyEnum';
export const baseCurrencyString = Currency.USD.toString();
export function convertMoneyToString({ currency, value }: Money) {
if (!value) {
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 + ')$');
};

@ -4619,7 +4619,7 @@ atob@^2.1.2:
attr-accept@^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==
autoprefixer@^10.3.7, autoprefixer@^10.4.12, autoprefixer@^10.4.7:
@ -7752,7 +7752,7 @@ file-loader@^6.0.0, file-loader@^6.2.0:
file-selector@^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==
dependencies:
tslib "^2.4.0"
@ -12188,7 +12188,7 @@ react-dom@18.2.0, react-dom@^18.2.0:
react-dropzone@^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==
dependencies:
attr-accept "^2.2.2"
@ -14192,47 +14192,47 @@ tty-browserify@0.0.0:
resolved "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz"
integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==
turbo-darwin-64@1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.5.5.tgz#710d4e7999066bd4f500456f7cd1c30f6e6205ed"
integrity sha512-HvEn6P2B+NXDekq9LRpRgUjcT9/oygLTcK47U0qsAJZXRBSq/2hvD7lx4nAwgY/4W3rhYJeWtHTzbhoN6BXqGQ==
turbo-darwin-64@1.5.6:
version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.5.6.tgz#2e0e14343c84dde33b5a09ea5389ee6a9565779c"
integrity sha512-CWdXMwenBS2+QXIR2Czx7JPnAcoMzWx/QwTDcHVxZyeayMHgz8Oq5AHCtfaHDSfV8YhD3xa0GLSk6+cFt+W8BQ==
turbo-darwin-arm64@1.5.5:
version "1.5.5"
resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.5.5.tgz"
integrity sha512-Dmxr09IUy6M0nc7/xWod9galIO2DD500B75sJSkHeT+CCdJOWnlinux0ZPF8CSygNqymwYO8AO2l15/6yxcycg==
turbo-darwin-arm64@1.5.6:
version "1.5.6"
resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.5.6.tgz"
integrity sha512-c/aXgW9JuXT2bJSKf01pdSDQKnrdcdj3WFKmKiVldb9We6eqFzI0fLHBK97k5LM/OesmRMfCMQ2Cv2DU8RqBAA==
turbo-linux-64@1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.5.5.tgz#f31eb117a9b605f5731048c50473bff903850047"
integrity sha512-wd07TZ4zXXWjzZE00FcFMLmkybQQK/NV9ff66vvAV0vdiuacSMBCNLrD6Mm4ncfrUPW/rwFW5kU/7hyuEqqtDw==
turbo-linux-64@1.5.6:
version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.5.6.tgz#e7ddaf7a87084dfdd9c6d79efb41084d75439b31"
integrity sha512-y/jNF7SG+XJEwk2GxIqy3g4dj/a0PgZKDGyOkp24qp4KBRcHBl6dI1ZEfNed30EhEqmW4F5Dr7IpeCZoqgbrMg==
turbo-linux-arm64@1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.5.5.tgz#b9ce6912ae6477e829355d6f012500bfef58669d"
integrity sha512-q3q33tuo74R7gicnfvFbnZZvqmlq7Vakcvx0eshifnJw4PR+oMnTCb4w8ElVFx070zsb8DVTibq99y8NJH8T1Q==
turbo-linux-arm64@1.5.6:
version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.5.6.tgz#6445f00f84e0f356a6a369ba2d75ede43aaeb796"
integrity sha512-FRcxPtW7eFrbR3QaYBVX8cK7i+2Cerqi6F0t5ulcq+d1OGSdSW3l35rPPyJdwCzCy+k/S9sBcyCV0RtbS6RKCQ==
turbo-windows-64@1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.5.5.tgz#609098de3bc6178f733615d21b06d5c1602637eb"
integrity sha512-lPp9kHonNFfqgovbaW+UAPO5cLmoAN+m3G3FzqcrRPnlzt97vXYsDhDd/4Zy3oAKoAcprtP4CGy0ddisqsKTVw==
turbo-windows-64@1.5.6:
version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.5.6.tgz#3638d5297319157031e4dc906dbae53a1db8562c"
integrity sha512-/5KIExY7zbrbeL5fhKGuO85u5VtJ3Ue4kI0MbYCNnTGe7a10yTYkwswgtGihsgEF4AW0Nm0159aHmXZS2Le8IA==
turbo-windows-arm64@1.5.5:
version "1.5.5"
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.5.5.tgz#60522e1e347a54c64bdddb68089fc322ee19c3d7"
integrity sha512-3AfGULKNZiZVrEzsIE+W79ZRW1+f5r4nM4wLlJ1PTBHyRxBZdD6KTH1tijGfy/uTlcV5acYnKHEkDc6Q9PAXGQ==
turbo-windows-arm64@1.5.6:
version "1.5.6"
resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.5.6.tgz#9eff9d13721be0b905b0aad07667507380f738fe"
integrity sha512-p+LQN9O39+rZuOAyc6BzyVGvdEKo+v+XmtdeyZsZpfj4xuOLtsEptW1w6cUD439u0YcPknuccGq1MQ0lXQ6Xuw==
turbo@latest:
version "1.5.5"
resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.5.5.tgz#9fc3a917c914ffa113c260a4eadb4bc632eee227"
integrity sha512-PVQSDl0STC9WXIyHcYUWs9gXsf8JjQig/FuHfuB8N6+XlgCGB3mPbfMEE6zrChGz2hufH4/guKRX1XJuNL6XTA==
version "1.5.6"
resolved "https://registry.npmjs.org/turbo/-/turbo-1.5.6.tgz"
integrity sha512-xJO/fhiMo4lI62iGR9OgUfJTC9tnnuoMwNC52IfvvBDEPlA8RWGMS8SFpDVG9bNCXvVRrtUTNJXMe6pJWBiOTA==
optionalDependencies:
turbo-darwin-64 "1.5.5"
turbo-darwin-arm64 "1.5.5"
turbo-linux-64 "1.5.5"
turbo-linux-arm64 "1.5.5"
turbo-windows-64 "1.5.5"
turbo-windows-arm64 "1.5.5"
turbo-darwin-64 "1.5.6"
turbo-darwin-arm64 "1.5.6"
turbo-linux-64 "1.5.6"
turbo-linux-arm64 "1.5.6"
turbo-windows-64 "1.5.6"
turbo-windows-arm64 "1.5.6"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"
@ -14670,7 +14670,7 @@ uuid-browser@^3.1.0:
uuid@^3.3.2:
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==
uuid@^8.3.2:

Loading…
Cancel
Save