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

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

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

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

@ -1,18 +1,11 @@
import clsx from 'clsx'; import clsx from 'clsx';
import type { Dispatch, SetStateAction } from 'react';
import { useState } from 'react'; import { useState } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import { ChevronUpIcon } from '@heroicons/react/20/solid';
import { useForm } from 'react-hook-form';
import {
ArrowDownCircleIcon,
ArrowUpCircleIcon,
} from '@heroicons/react/20/solid';
import { FaceSmileIcon } from '@heroicons/react/24/outline'; 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 ResumeUserBadges from '../badges/ResumeUserBadges';
import ResumeExpandableText from '../shared/ResumeExpandableText'; import ResumeExpandableText from '../shared/ResumeExpandableText';
@ -23,141 +16,55 @@ type ResumeCommentListItemProps = {
userId: string | undefined; userId: string | undefined;
}; };
type ICommentInput = {
description: string;
};
export default function ResumeCommentListItem({ export default function ResumeCommentListItem({
comment, comment,
userId, userId,
}: ResumeCommentListItemProps) { }: ResumeCommentListItemProps) {
const isCommentOwner = userId === comment.user.userId; const isCommentOwner = userId === comment.user.userId;
const [isEditingComment, setIsEditingComment] = useState(false); const [isEditingComment, setIsEditingComment] = useState(false);
const [isReplyingComment, setIsReplyingComment] = useState(false);
const [upvoteAnimation, setUpvoteAnimation] = useState(false); const [showReplies, setShowReplies] = useState(true);
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),
},
);
};
return ( 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"> <div className="flex flex-row space-x-2 p-1 align-top">
{/* Image Icon */}
{comment.user.image ? ( {comment.user.image ? (
<img <img
alt={comment.user.name ?? 'Reviewer'} 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!} 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"> <div className="flex w-full flex-col space-y-1">
{/* Name and creation time */} {/* Name and creation time */}
<div className="flex flex-row justify-between"> <div className="flex flex-row justify-between">
<div className="flex flex-row items-center space-x-1"> <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'} {comment.user.name ?? 'Reviewer ABC'}
</p> </p>
<p className="text-primary-800 text-xs font-medium"> <p className="text-xs font-medium text-indigo-800">
{isCommentOwner ? '(Me)' : ''} {isCommentOwner ? '(Me)' : ''}
</p> </p>
@ -174,112 +81,92 @@ export default function ResumeCommentListItem({
{/* Description */} {/* Description */}
{isEditingComment ? ( {isEditingComment ? (
<form onSubmit={handleSubmit(onSubmit)}> <ResumeCommentEditForm
<div className="flex-column mt-1 space-y-2"> comment={comment}
<TextArea setIsEditingComment={setIsEditingComment}
{...(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>
) : ( ) : (
<ResumeExpandableText text={comment.description} /> <ResumeExpandableText
key={comment.description}
text={comment.description}
/>
)} )}
{/* Upvote and edit */} {/* Upvote and edit */}
<div className="flex flex-row space-x-1 pt-1 align-middle"> <div className="flex flex-row space-x-1 pt-1 align-middle">
<button <ResumeCommentVoteButtons commentId={comment.id} userId={userId} />
disabled={
!userId || {/* Action buttons; only present for authenticated user when not editing/replying */}
commentVotesQuery.isLoading || {userId && !isEditingComment && !isReplyingComment && (
commentVotesUpsertMutation.isLoading || <>
commentVotesDeleteMutation.isLoading {isCommentOwner && (
} <button
type="button" className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
onClick={() => onVote(Vote.UPVOTE, setUpvoteAnimation)}> type="button"
<ArrowUpCircleIcon onClick={() => setIsEditingComment(true)}>
className={clsx( Edit
'h-4 w-4', </button>
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="text-xs"> {!comment.parentId && (
{commentVotesQuery.data?.numVotes ?? 0} <button
</div> className="px-1 text-xs text-indigo-800 hover:text-indigo-400"
type="button"
<button onClick={() => setIsReplyingComment(true)}>
disabled={ Reply
!userId || </button>
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> )}
</div>
{isCommentOwner && !isEditingComment && ( {/* 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 <button
className="text-primary-800 hover:text-primary-400 px-1 text-xs" className="flex items-center space-x-1 rounded-md text-xs font-medium text-indigo-800 hover:text-indigo-300"
type="button" type="button"
onClick={() => setIsEditingComment(true)}> onClick={() => setShowReplies(!showReplies)}>
Edit <ChevronUpIcon
className={clsx(
'h-5 w-5 ',
!showReplies && 'rotate-180 transform',
)}
/>
<span>{showReplies ? 'Hide replies' : 'Show replies'}</span>
</button> </button>
)}
</div> {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> </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 clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useLayoutEffect, useRef, useState } from 'react';
type ResumeExpandableTextProps = Readonly<{ type ResumeExpandableTextProps = Readonly<{
text: string; text: string;
@ -8,17 +8,17 @@ type ResumeExpandableTextProps = Readonly<{
export default function ResumeExpandableText({ export default function ResumeExpandableText({
text, text,
}: ResumeExpandableTextProps) { }: ResumeExpandableTextProps) {
const ref = useRef<HTMLSpanElement>(null);
const [isExpanded, setIsExpanded] = useState(false); const [isExpanded, setIsExpanded] = useState(false);
const [descriptionOverflow, setDescriptionOverflow] = useState(false); const [descriptionOverflow, setDescriptionOverflow] = useState(false);
useEffect(() => { useLayoutEffect(() => {
const lines = text.split(/\r\n|\r|\n/); if (ref.current && ref.current.clientHeight < ref.current.scrollHeight) {
if (lines.length > 3) {
setDescriptionOverflow(true); setDescriptionOverflow(true);
} else { } else {
setDescriptionOverflow(false); setDescriptionOverflow(false);
} }
}, [text]); }, [ref]);
const onSeeActionClicked = () => { const onSeeActionClicked = () => {
setIsExpanded((prevExpanded) => !prevExpanded); setIsExpanded((prevExpanded) => !prevExpanded);
@ -27,6 +27,7 @@ export default function ResumeExpandableText({
return ( return (
<div> <div>
<span <span
ref={ref}
className={clsx( className={clsx(
'line-clamp-3 whitespace-pre-wrap text-sm', 'line-clamp-3 whitespace-pre-wrap text-sm',
isExpanded ? 'line-clamp-none' : '', isExpanded ? 'line-clamp-none' : '',

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

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

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

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

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

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

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

@ -131,7 +131,9 @@ export default function ResumeReviewPage() {
onClick={onStarButtonClick}> onClick={onStarButtonClick}>
<span className="relative inline-flex"> <span className="relative inline-flex">
<div className="-ml-1 mr-2 h-5 w-5"> <div className="-ml-1 mr-2 h-5 w-5">
{starMutation.isLoading || unstarMutation.isLoading ? ( {starMutation.isLoading ||
unstarMutation.isLoading ||
detailsQuery.isLoading ? (
<Spinner className="mt-0.5" size="xs" /> <Spinner className="mt-0.5" size="xs" />
) : ( ) : (
<StarIcon <StarIcon
@ -198,7 +200,10 @@ export default function ResumeReviewPage() {
aria-hidden="true" aria-hidden="true"
className="mr-1.5 h-5 w-5 flex-shrink-0 text-gray-400" 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>
)} )}
<div className="flex w-full flex-col py-4 lg:flex-row"> <div className="flex w-full flex-col py-4 lg:flex-row">

@ -1,9 +1,10 @@
import Head from 'next/head'; import Head from 'next/head';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import { useEffect, useState } from 'react'; import { Fragment, useEffect, useState } from 'react';
import { Disclosure } from '@headlessui/react'; import { Dialog, Disclosure, Transition } from '@headlessui/react';
import { MinusIcon, PlusIcon } from '@heroicons/react/20/solid'; import { FunnelIcon, MinusIcon, PlusIcon } from '@heroicons/react/20/solid';
import { XMarkIcon } from '@heroicons/react/24/outline';
import { import {
MagnifyingGlassIcon, MagnifyingGlassIcon,
NewspaperIcon, NewspaperIcon,
@ -13,6 +14,7 @@ import {
CheckboxList, CheckboxList,
DropdownMenu, DropdownMenu,
Pagination, Pagination,
Spinner,
Tabs, Tabs,
TextInput, TextInput,
} from '@tih/ui'; } from '@tih/ui';
@ -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 = ( const getEmptyDataText = (
tabsValue: string, tabsValue: string,
searchValue: string, searchValue: string,
@ -76,11 +89,11 @@ const getEmptyDataText = (
case BROWSE_TABS_VALUES.ALL: case BROWSE_TABS_VALUES.ALL:
return 'Looks like SWEs are feeling lucky!'; return 'Looks like SWEs are feeling lucky!';
case BROWSE_TABS_VALUES.STARRED: 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: case BROWSE_TABS_VALUES.MY:
return 'Upload a resume to see it here!'; return 'Upload a resume to see it here!';
default: default:
return null; return '';
} }
}; };
@ -92,9 +105,8 @@ export default function ResumeHomePage() {
const [searchValue, setSearchValue] = useState(''); const [searchValue, setSearchValue] = useState('');
const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE); const [userFilters, setUserFilters] = useState(INITIAL_FILTER_STATE);
const [shortcutSelected, setShortcutSelected] = useState('All'); const [shortcutSelected, setShortcutSelected] = useState('All');
const [renderSignInButton, setRenderSignInButton] = useState(false);
const [signInButtonText, setSignInButtonText] = useState('');
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [mobileFiltersOpen, setMobileFiltersOpen] = useState(false);
const skip = (currentPage - 1) * PAGE_LIMIT; const skip = (currentPage - 1) * PAGE_LIMIT;
@ -117,9 +129,6 @@ export default function ResumeHomePage() {
], ],
{ {
enabled: tabsValue === BROWSE_TABS_VALUES.ALL, enabled: tabsValue === BROWSE_TABS_VALUES.ALL,
onSuccess: () => {
setRenderSignInButton(false);
},
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}, },
); );
@ -138,10 +147,6 @@ export default function ResumeHomePage() {
], ],
{ {
enabled: tabsValue === BROWSE_TABS_VALUES.STARRED, enabled: tabsValue === BROWSE_TABS_VALUES.STARRED,
onError: () => {
setRenderSignInButton(true);
setSignInButtonText('to view starred resumes');
},
retry: false, retry: false,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}, },
@ -161,20 +166,16 @@ export default function ResumeHomePage() {
], ],
{ {
enabled: tabsValue === BROWSE_TABS_VALUES.MY, enabled: tabsValue === BROWSE_TABS_VALUES.MY,
onError: () => {
setRenderSignInButton(true);
setSignInButtonText('to view your submitted resumes');
},
retry: false, retry: false,
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
}, },
); );
const onSubmitResume = () => { const onSubmitResume = () => {
if (sessionData?.user?.id) { if (sessionData === null) {
router.push('/resumes/submit');
} else {
router.push('/api/auth/signin'); router.push('/api/auth/signin');
} else {
router.push('/resumes/submit');
} }
}; };
@ -242,88 +243,51 @@ export default function ResumeHomePage() {
<Head> <Head>
<title>Resume Review Portal</title> <title>Resume Review Portal</title>
</Head> </Head>
<main className="h-[calc(100vh-4rem)] flex-1 overflow-y-scroll">
<div className="ml-4 py-4"> {/* Mobile Filters */}
<ResumeReviewsTitle /> <div>
</div> <Transition.Root as={Fragment} show={mobileFiltersOpen}>
<div className="mt-4 flex items-start"> <Dialog
<div className="w-screen sm:px-4 md:px-8"> as="div"
<div className="grid grid-cols-12"> className="relative z-40 lg:hidden"
<div className="col-span-2 self-end"> onClose={setMobileFiltersOpen}>
<h3 className="text-md mb-4 font-medium tracking-tight text-gray-900"> <Transition.Child
Shortcuts: as={Fragment}
</h3> enter="transition-opacity ease-linear duration-300"
</div> enterFrom="opacity-0"
<div className="col-span-10"> enterTo="opacity-100"
<div className="border-grey-200 grid grid-cols-12 items-center gap-4 border-b pb-2"> leave="transition-opacity ease-linear duration-300"
<div className="col-span-5"> leaveFrom="opacity-100"
<Tabs leaveTo="opacity-0">
label="Resume Browse Tabs" <div className="fixed inset-0 bg-black bg-opacity-25" />
tabs={[ </Transition.Child>
{
label: 'All Resumes', <div className="fixed inset-0 z-40 flex">
value: BROWSE_TABS_VALUES.ALL, <Transition.Child
}, as={Fragment}
{ enter="transition ease-in-out duration-300 transform"
label: 'Starred Resumes', enterFrom="translate-x-full"
value: BROWSE_TABS_VALUES.STARRED, enterTo="translate-x-0"
}, leave="transition ease-in-out duration-300 transform"
{ leaveFrom="translate-x-0"
label: 'My Resumes', leaveTo="translate-x-full">
value: BROWSE_TABS_VALUES.MY, <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">
value={tabsValue} Shortcuts
onChange={onTabChange} </h2>
/> <button
</div> className="-mr-2 flex h-10 w-10 items-center justify-center rounded-md bg-white p-2 text-gray-400"
<div className="col-span-7 flex items-center justify-evenly"> type="button"
<div className="w-64"> onClick={() => setMobileFiltersOpen(false)}>
<form> <span className="sr-only">Close menu</span>
<TextInput <XMarkIcon aria-hidden="true" className="h-6 w-6" />
label="" </button>
placeholder="Search Resumes"
startAddOn={MagnifyingGlassIcon}
startAddOnType="icon"
type="text"
value={searchValue}
onChange={setSearchValue}
/>
</form>
</div>
<div>
<DropdownMenu align="end" label={SORT_OPTIONS[sortOrder]}>
{Object.entries(SORT_OPTIONS).map(([key, value]) => (
<DropdownMenu.Item
key={key}
isSelected={sortOrder === key}
label={value}
onClick={() =>
setSortOrder(key)
}></DropdownMenu.Item>
))}
</DropdownMenu>
</div>
<div>
<button
className="rounded-md bg-indigo-500 py-2 px-3 text-sm font-medium text-white"
type="button"
onClick={onSubmitResume}>
Submit Resume
</button>
</div>
</div> </div>
</div>
</div>
</div>
<div className="grid grid-cols-12"> <form className="mt-4 border-t border-gray-200">
<div className="col-span-2">
<div className="w-100 pt-4 sm:pr-0 md:pr-4">
<form>
<h3 className="sr-only">Shortcuts</h3>
<ul <ul
className="flex flex-wrap justify-start gap-4 pb-6 text-sm font-medium text-gray-900" className="flex flex-wrap justify-start gap-4 px-4 py-3 font-medium text-gray-900"
role="list"> role="list">
{SHORTCUTS.map((shortcut) => ( {SHORTCUTS.map((shortcut) => (
<li key={shortcut.name}> <li key={shortcut.name}>
@ -335,18 +299,16 @@ export default function ResumeHomePage() {
</li> </li>
))} ))}
</ul> </ul>
<h3 className="text-md my-4 font-medium tracking-tight text-gray-900">
Explore these filters:
</h3>
{filters.map((filter) => ( {filters.map((filter) => (
<Disclosure <Disclosure
key={filter.id} key={filter.id}
as="div" as="div"
className="border-b border-gray-200 py-6"> className="border-t border-gray-200 px-4 py-6">
{({ open }) => ( {({ open }) => (
<> <>
<h3 className="-my-3 flow-root"> <h3 className="-mx-2 -my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm text-gray-400 hover:text-gray-500"> <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"> <span className="font-medium text-gray-900">
{filter.label} {filter.label}
</span> </span>
@ -365,12 +327,8 @@ export default function ResumeHomePage() {
</span> </span>
</Disclosure.Button> </Disclosure.Button>
</h3> </h3>
<Disclosure.Panel className="pt-4"> <Disclosure.Panel className="pt-6">
<CheckboxList <div className="space-y-6">
description=""
isLabelHidden={true}
label=""
orientation="vertical">
{filter.options.map((option) => ( {filter.options.map((option) => (
<div <div
key={option.value} key={option.value}
@ -390,51 +348,231 @@ export default function ResumeHomePage() {
/> />
</div> </div>
))} ))}
</CheckboxList> </div>
</Disclosure.Panel> </Disclosure.Panel>
</> </>
)} )}
</Disclosure> </Disclosure>
))} ))}
</form> </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="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>
<ul
className="flex flex-wrap justify-start gap-4 pb-6 text-sm 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>
<h3 className="text-md my-4 font-medium tracking-tight text-gray-900">
Explore these filters:
</h3>
{filters.map((filter) => (
<Disclosure
key={filter.id}
as="div"
className="border-b border-gray-200 py-6">
{({ open }) => (
<>
<h3 className="-my-3 flow-root">
<Disclosure.Button className="flex w-full items-center justify-between py-3 text-sm 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-4">
<CheckboxList
description=""
isLabelHidden={true}
label=""
orientation="vertical">
{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>
))}
</CheckboxList>
</Disclosure.Panel>
</>
)}
</Disclosure>
))}
</form>
</div>
</div>
<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> </div>
<div className="col-span-10 mb-6"> <div className="flex flex-wrap items-center justify-start gap-8">
{renderSignInButton && ( <div className="w-64">
<ResumeSignInButton text={signInButtonText} /> <form>
)} <TextInput
{getTabResumes().length === 0 ? ( label=""
<div className="mt-24 flex flex-wrap justify-center"> placeholder="Search Resumes"
<NewspaperIcon startAddOn={MagnifyingGlassIcon}
className="mb-12 basis-full" startAddOnType="icon"
height={196} type="text"
width={196} value={searchValue}
/> onChange={setSearchValue}
{getEmptyDataText(tabsValue, searchValue, userFilters)}
</div>
) : (
<>
<ResumeListItems
isLoading={
allResumesQuery.isFetching ||
starredResumesQuery.isFetching ||
myResumesQuery.isFetching
}
resumes={getTabResumes()}
/> />
<div className="my-4 flex justify-center"> </form>
<Pagination </div>
current={currentPage} <div>
end={getTabTotalPages()} <DropdownMenu align="end" label={SORT_OPTIONS[sortOrder]}>
label="pagination" {Object.entries(SORT_OPTIONS).map(([key, value]) => (
start={1} <DropdownMenu.Item
onSelect={(page) => setCurrentPage(page)} key={key}
/> isSelected={sortOrder === key}
</div> 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> </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"
height={196}
width={196}
/>
{getEmptyDataText(tabsValue, searchValue, userFilters)}
</div>
) : (
<>
<ResumeListItems
isLoading={
allResumesQuery.isFetching ||
starredResumesQuery.isFetching ||
myResumesQuery.isFetching
}
resumes={getTabResumes()}
/>
<div className="my-4 flex justify-center">
<Pagination
current={currentPage}
end={getTabTotalPages()}
label="pagination"
start={1}
onSelect={(page) => setCurrentPage(page)}
/>
</div>
</>
)}
</div>
</div> </div>
</div> </div>
</main> </main>

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

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

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

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

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

@ -5,18 +5,33 @@ import { TRPCError } from '@trpc/server';
import { createProtectedRouter } from './context'; import { createProtectedRouter } from './context';
import type { Question } from '~/types/questions'; import type { Question } from '~/types/questions';
import { SortOrder, SortType } from '~/types/questions.d';
const TWO_WEEK_IN_MS = 12096e5;
export const questionsQuestionRouter = createProtectedRouter() export const questionsQuestionRouter = createProtectedRouter()
.query('getQuestionsByFilter', { .query('getQuestionsByFilter', {
input: z.object({ input: z.object({
companyNames: z.string().array(), companyNames: z.string().array(),
endDate: z.date(), endDate: z.date().default(new Date()),
locations: z.string().array(), locations: z.string().array(),
pageSize: z.number().default(50),
questionTypes: z.nativeEnum(QuestionsQuestionType).array(), questionTypes: z.nativeEnum(QuestionsQuestionType).array(),
roles: z.string().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 }) { async resolve({ ctx, input }) {
const sortCondition =
input.sortType === SortType.TOP
? {
upvotes: input.sortOrder,
}
: {
lastSeenAt: input.sortOrder,
};
const questionsData = await ctx.prisma.questionsQuestion.findMany({ const questionsData = await ctx.prisma.questionsQuestion.findMany({
include: { include: {
_count: { _count: {
@ -41,7 +56,7 @@ export const questionsQuestionRouter = createProtectedRouter()
votes: true, votes: true,
}, },
orderBy: { orderBy: {
createdAt: 'desc', ...sortCondition,
}, },
where: { where: {
...(input.questionTypes.length > 0 ...(input.questionTypes.length > 0
@ -53,6 +68,10 @@ export const questionsQuestionRouter = createProtectedRouter()
: {}), : {}),
encounters: { encounters: {
some: { some: {
seenAt: {
gte: input.startDate,
lte: input.endDate,
},
...(input.companyNames.length > 0 ...(input.companyNames.length > 0
? { ? {
company: { company: {
@ -207,24 +226,23 @@ export const questionsQuestionRouter = createProtectedRouter()
data: { data: {
content: input.content, content: input.content,
encounters: { encounters: {
create: [ create: {
{ company: {
company: { connect: {
connect: { id: input.companyId,
id: input.companyId,
},
}, },
location: input.location, },
role: input.role, location: input.location,
seenAt: input.seenAt, role: input.role,
user: { seenAt: input.seenAt,
connect: { user: {
id: userId, connect: {
}, id: userId,
}, },
}, },
], },
}, },
lastSeenAt: input.seenAt,
questionType: input.questionType, questionType: input.questionType,
userId, userId,
}, },
@ -319,13 +337,28 @@ export const questionsQuestionRouter = createProtectedRouter()
const userId = ctx.session?.user?.id; const userId = ctx.session?.user?.id;
const { questionId, vote } = input; const { questionId, vote } = input;
return await ctx.prisma.questionsQuestionVote.create({ const incrementValue = vote === Vote.UPVOTE ? 1 : -1;
data: {
questionId, const [questionVote] = await ctx.prisma.$transaction([
userId, ctx.prisma.questionsQuestionVote.create({
vote, data: {
}, questionId,
}); userId,
vote,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: questionId,
},
}),
]);
return questionVote;
}, },
}) })
.mutation('updateVote', { .mutation('updateVote', {
@ -350,14 +383,30 @@ export const questionsQuestionRouter = createProtectedRouter()
}); });
} }
return await ctx.prisma.questionsQuestionVote.update({ const incrementValue = vote === Vote.UPVOTE ? 2 : -2;
data: {
vote, const [questionVote] = await ctx.prisma.$transaction([
}, ctx.prisma.questionsQuestionVote.update({
where: { data: {
id, vote,
}, },
}); where: {
id,
},
}),
ctx.prisma.questionsQuestion.update({
data: {
upvotes: {
increment: incrementValue,
},
},
where: {
id: voteToUpdate.questionId,
},
}),
]);
return questionVote;
}, },
}) })
.mutation('deleteVote', { .mutation('deleteVote', {
@ -380,10 +429,25 @@ export const questionsQuestionRouter = createProtectedRouter()
}); });
} }
return await ctx.prisma.questionsQuestionVote.delete({ const incrementValue = voteToDelete.vote === Vote.UPVOTE ? -1 : 1;
where: {
id: input.id, 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 // The user's name and image to render
const comments = await ctx.prisma.resumesComment.findMany({ const comments = await ctx.prisma.resumesComment.findMany({
include: { include: {
children: {
include: {
user: {
select: {
image: true,
name: true,
},
},
},
orderBy: {
createdAt: 'asc',
},
},
user: { user: {
select: { select: {
image: true, image: true,
@ -26,15 +39,35 @@ export const resumeCommentsRouter = createRouter().query('list', {
createdAt: 'desc', createdAt: 'desc',
}, },
where: { where: {
resumeId, AND: [{ resumeId }, { parentId: null }],
}, },
}); });
return comments.map((data) => { 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 = { const comment: ResumeComment = {
children,
createdAt: data.createdAt, createdAt: data.createdAt,
description: data.description, description: data.description,
id: data.id, id: data.id,
parentId: data.parentId,
resumeId: data.resumeId, resumeId: data.resumeId,
section: data.section, section: data.section,
updatedAt: data.updatedAt, 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; let topUpvotedCommentCount = 0;
for (const resume of resumes) { 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 // Get Map of {userId, voteCount} for each comment
const commentUpvotePairs = []; const commentUpvotePairs = [];

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

@ -51,3 +51,13 @@ export type QuestionComment = {
user: string; user: string;
userImage: 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 * frontend-friendly representation of the query
*/ */
export type ResumeComment = Readonly<{ export type ResumeComment = Readonly<{
children: Array<ResumeComment>;
createdAt: Date; createdAt: Date;
description: string; description: string;
id: string; id: string;
parentId: string?;
resumeId: string; resumeId: string;
section: ResumesSection; section: ResumesSection;
updatedAt: Date; updatedAt: Date;

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

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

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

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

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

Loading…
Cancel
Save